%%%-------------------------------------------------------------------
%%% File    : mod_muc_mnesia.erl
%%% Author  : Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2024   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

-module(mod_muc_mnesia).

-behaviour(mod_muc).
-behaviour(mod_muc_room).

%% API
-export([init/2, import/3, store_room/5, restore_room/3, forget_room/3,
	 can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]).
-export([register_online_room/4, unregister_online_room/4, find_online_room/3,
	 get_online_rooms/3, count_online_rooms/2, rsm_supported/0,
	 register_online_user/4, unregister_online_user/4,
	 count_online_rooms_by_user/3, get_online_rooms_by_user/3,
	 find_online_room_by_pid/2]).
-export([set_affiliation/6, set_affiliations/4, get_affiliation/5,
	 get_affiliations/3, search_affiliation/4]).
%% gen_server callbacks
-export([start_link/2, init/1, handle_cast/2, handle_call/3, handle_info/2,
	 terminate/2, code_change/3]).
-export([need_transform/1, transform/1]).

-include("mod_muc.hrl").
-include("logger.hrl").
-include_lib("xmpp/include/xmpp.hrl").
-include_lib("stdlib/include/ms_transform.hrl").

-record(state, {}).

%%%===================================================================
%%% API
%%%===================================================================
init(Host, Opts) ->
    Spec = {?MODULE, {?MODULE, start_link, [Host, Opts]},
	    transient, 5000, worker, [?MODULE]},
    case supervisor:start_child(ejabberd_backend_sup, Spec) of
	{ok, _Pid} -> ok;
        %% Maybe started for a vhost which only wanted mnesia for ram
        %% and this vhost wants mnesia for persitent storage too
        {error, {already_started, _Pid}} ->
            init([Host, Opts]);
	Err -> Err
    end.

start_link(Host, Opts) ->
    Name = gen_mod:get_module_proc(Host, ?MODULE),
    gen_server:start_link({local, Name}, ?MODULE, [Host, Opts], []).

store_room(_LServer, Host, Name, Opts, _) ->
    F = fun () ->
		mnesia:write(#muc_room{name_host = {Name, Host},
				       opts = Opts})
	end,
    mnesia:transaction(F).

restore_room(_LServer, Host, Name) ->
    case catch mnesia:dirty_read(muc_room, {Name, Host}) of
	[#muc_room{opts = Opts}] -> Opts;
	_ -> error
    end.

forget_room(_LServer, Host, Name) ->
    F = fun () -> mnesia:delete({muc_room, {Name, Host}})
	end,
    mnesia:transaction(F).

can_use_nick(_LServer, ServiceOrRoom, JID, Nick) ->
    {LUser, LServer, _} = jid:tolower(JID),
    LUS = {LUser, LServer},
    MatchSpec = case (jid:decode(ServiceOrRoom))#jid.lserver of
        ServiceOrRoom -> [{'==', {element, 2, '$1'}, ServiceOrRoom}];
        Service -> [{'orelse',
                    {'==', {element, 2, '$1'}, Service},
                    {'==', {element, 2, '$1'}, ServiceOrRoom} }]
                end,
    case catch mnesia:dirty_select(muc_registered,
				   [{#muc_registered{us_host = '$1',
						     nick = Nick, _ = '_'},
				     MatchSpec,
				     ['$_']}])
	of
      {'EXIT', _Reason} -> true;
      [] -> true;
      [#muc_registered{us_host = {U, _Host}}] -> U == LUS
    end.

get_rooms(_LServer, Host) ->
    mnesia:dirty_select(muc_room,
			[{#muc_room{name_host = {'_', Host},
				    _ = '_'},
			  [], ['$_']}]).

get_nick(_LServer, Host, From) ->
    {LUser, LServer, _} = jid:tolower(From),
    LUS = {LUser, LServer},
    case mnesia:dirty_read(muc_registered, {LUS, Host}) of
	[] -> error;
	[#muc_registered{nick = Nick}] -> Nick
    end.

set_nick(_LServer, ServiceOrRoom, From, Nick) ->
    {LUser, LServer, _} = jid:tolower(From),
    LUS = {LUser, LServer},
    F = fun () ->
		case Nick of
		    <<"">> ->
			mnesia:delete({muc_registered, {LUS, ServiceOrRoom}}),
			ok;
		    _ ->
                        Service = (jid:decode(ServiceOrRoom))#jid.lserver,
                        MatchSpec = case (ServiceOrRoom == Service) of
                            true -> [{'==', {element, 2, '$1'}, ServiceOrRoom}];
                            false -> [{'orelse',
                                        {'==', {element, 2, '$1'}, Service},
                                        {'==', {element, 2, '$1'}, ServiceOrRoom} }]
                                    end,
			Allow = case mnesia:select(
				       muc_registered,
				       [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'},
					 MatchSpec,
					 ['$_']}]) of
				    [] when (ServiceOrRoom == Service) ->
			                NickRegistrations = mnesia:select(
				            muc_registered,
				            [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'},
					        [],
					        ['$_']}]),
                                        not lists:any(fun({_, {_NRUS, NRServiceOrRoom}, _Nick}) ->
                                                              Service == (jid:decode(NRServiceOrRoom))#jid.lserver end,
                                                      NickRegistrations);
				    [] -> true;
				    [#muc_registered{us_host = {_U, Host}}]
                                      when (Host == Service) and (ServiceOrRoom /= Service) ->
					false;
				    [#muc_registered{us_host = {U, _Host}}] ->
					U == LUS
				end,
			if Allow ->
				mnesia:write(#muc_registered{
						us_host = {LUS, ServiceOrRoom},
						nick = Nick}),
				ok;
			   true ->
				false
			end
		end
	end,
    mnesia:transaction(F).

set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) ->
    {error, not_implemented}.

set_affiliations(_ServerHost, _Room, _Host, _Affiliations) ->
    {error, not_implemented}.

get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) ->
    {error, not_implemented}.

get_affiliations(_ServerHost, _Room, _Host) ->
    {error, not_implemented}.

search_affiliation(_ServerHost, _Room, _Host, _Affiliation) ->
    {error, not_implemented}.

register_online_room(_ServerHost, Room, Host, Pid) ->
    F = fun() ->
		mnesia:write(
		  #muc_online_room{name_host = {Room, Host}, pid = Pid})
	end,
    mnesia:transaction(F).

unregister_online_room(_ServerHost, Room, Host, Pid) ->
    F = fun () ->
		mnesia:delete_object(
		  #muc_online_room{name_host = {Room, Host}, pid = Pid})
	end,
    mnesia:transaction(F).

find_online_room(_ServerHost, Room, Host) ->
    find_online_room(Room, Host).

find_online_room(Room, Host) ->
    case mnesia:dirty_read(muc_online_room, {Room, Host}) of
	[] -> error;
	[#muc_online_room{pid = Pid}] -> {ok, Pid}
    end.

find_online_room_by_pid(_ServerHost, Pid) ->
    Res =
    mnesia:dirty_select(
	muc_online_room,
	ets:fun2ms(
	    fun(#muc_online_room{name_host = {Name, Host}, pid = PidS})
		   when PidS == Pid -> {Name, Host}
	    end)),
    case Res of
	[{Name, Host}] -> {ok, Name, Host};
	_ -> error
    end.

count_online_rooms(_ServerHost, Host) ->
    ets:select_count(
      muc_online_room,
      ets:fun2ms(
	fun(#muc_online_room{name_host = {_, H}}) ->
		H == Host
	end)).

get_online_rooms(_ServerHost, Host,
		 #rsm_set{max = Max, 'after' = After, before = undefined})
  when is_binary(After), After /= <<"">> ->
    lists:reverse(get_online_rooms(next, {After, Host}, Host, 0, Max, []));
get_online_rooms(_ServerHost, Host,
		 #rsm_set{max = Max, 'after' = undefined, before = Before})
  when is_binary(Before), Before /= <<"">> ->
    get_online_rooms(prev, {Before, Host}, Host, 0, Max, []);
get_online_rooms(_ServerHost, Host,
		 #rsm_set{max = Max, 'after' = undefined, before = <<"">>}) ->
    get_online_rooms(last, {<<"">>, Host}, Host, 0, Max, []);
get_online_rooms(_ServerHost, Host, #rsm_set{max = Max}) ->
    lists:reverse(get_online_rooms(first, {<<"">>, Host}, Host, 0, Max, []));
get_online_rooms(_ServerHost, Host, undefined) ->
    mnesia:dirty_select(
      muc_online_room,
      ets:fun2ms(
	fun(#muc_online_room{name_host = {Name, H}, pid = Pid})
	      when H == Host -> {Name, Host, Pid}
	end)).

-spec get_online_rooms(prev | next | last | first,
		       {binary(), binary()}, binary(),
		       non_neg_integer(), non_neg_integer() | undefined,
		       [{binary(), binary(), pid()}]) ->
			      [{binary(), binary(), pid()}].
get_online_rooms(_Action, _Key, _Host, Count, Max, Items) when Count >= Max ->
    Items;
get_online_rooms(Action, Key, Host, Count, Max, Items) ->
    Call = fun() ->
		   case Action of
		       prev -> mnesia:dirty_prev(muc_online_room, Key);
		       next -> mnesia:dirty_next(muc_online_room, Key);
		       last -> mnesia:dirty_last(muc_online_room);
		       first -> mnesia:dirty_first(muc_online_room)
		   end
	   end,
    NewAction = case Action of
		    last -> prev;
		    first -> next;
		    _ -> Action
		end,
    try Call() of
	'$end_of_table' ->
	    Items;
	{Room, Host} = NewKey ->
	    case find_online_room(Room, Host) of
		{ok, Pid} ->
		    get_online_rooms(NewAction, NewKey, Host,
				     Count + 1, Max, [{Room, Host, Pid}|Items]);
		error ->
		    get_online_rooms(NewAction, NewKey, Host,
				     Count, Max, Items)
	    end;
	NewKey ->
	    get_online_rooms(NewAction, NewKey, Host, Count, Max, Items)
    catch _:{aborted, {badarg, _}} ->
	    Items
    end.

rsm_supported() ->
    true.

register_online_user(_ServerHost, {U, S, R}, Room, Host) ->
    ets:insert(muc_online_users,
	       #muc_online_users{us = {U, S}, resource = R,
				 room = Room, host = Host}).

unregister_online_user(_ServerHost, {U, S, R}, Room, Host) ->
    ets:delete_object(muc_online_users,
		      #muc_online_users{us = {U, S}, resource = R,
					room = Room, host = Host}).

count_online_rooms_by_user(ServerHost, U, S) ->
    MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)),
    ets:select_count(
      muc_online_users,
      ets:fun2ms(
	fun(#muc_online_users{us = {U1, S1}, host = Host}) ->
		U == U1 andalso S == S1 andalso MucHost == Host
	end)).

get_online_rooms_by_user(ServerHost, U, S) ->
    MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)),
    ets:select(
      muc_online_users,
      ets:fun2ms(
	fun(#muc_online_users{us = {U1, S1}, room = Room, host = Host})
	      when U == U1 andalso S == S1 andalso MucHost == Host -> {Room, Host}
	end)).

import(_LServer, <<"muc_room">>,
       [Name, RoomHost, SOpts, _TimeStamp]) ->
    Opts = mod_muc:opts_to_binary(ejabberd_sql:decode_term(SOpts)),
    mnesia:dirty_write(
      #muc_room{name_host = {Name, RoomHost},
                opts = Opts});
import(_LServer, <<"muc_registered">>,
       [J, RoomHost, Nick, _TimeStamp]) ->
    #jid{user = U, server = S} = jid:decode(J),
    mnesia:dirty_write(
      #muc_registered{us_host = {{U, S}, RoomHost},
                      nick = Nick}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([_Host, Opts]) ->
    MyHosts = mod_muc_opt:hosts(Opts),
    case gen_mod:db_mod(Opts, mod_muc) of
	?MODULE ->
	    ejabberd_mnesia:create(?MODULE, muc_room,
				   [{disc_copies, [node()]},
				    {attributes,
				     record_info(fields, muc_room)}]),
	    ejabberd_mnesia:create(?MODULE, muc_registered,
				   [{disc_copies, [node()]},
				    {attributes,
				     record_info(fields, muc_registered)},
				    {index, [nick]}]);
	_ ->
	    ok
    end,
    case gen_mod:ram_db_mod(Opts, mod_muc) of
	?MODULE ->
	    ejabberd_mnesia:create(?MODULE, muc_online_room,
				   [{ram_copies, [node()]},
				    {type, ordered_set},
				    {attributes, record_info(fields, muc_online_room)}]),
	    catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
	    lists:foreach(
	      fun(MyHost) ->
		      clean_table_from_bad_node(node(), MyHost)
	      end, MyHosts),
	    mnesia:subscribe(system);
	_ ->
	    ok
    end,
    {ok, #state{}}.

handle_call(Request, From, State) ->
    ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
    {noreply, State}.

handle_cast(Msg, State) ->
    ?WARNING_MSG("Unexpected cast: ~p", [Msg]),
    {noreply, State}.

handle_info({mnesia_system_event, {mnesia_down, Node}}, State) ->
    clean_table_from_bad_node(Node),
    {noreply, State};
handle_info({mnesia_system_event, {mnesia_up, _Node}}, State) ->
    {noreply, State};
handle_info(Info, State) ->
    ?WARNING_MSG("Unexpected info: ~p", [Info]),
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
clean_table_from_bad_node(Node) ->
    F = fun() ->
		Es = mnesia:select(
		       muc_online_room,
		       [{#muc_online_room{pid = '$1', _ = '_'},
			 [{'==', {node, '$1'}, Node}],
			 ['$_']}]),
		lists:foreach(fun(E) ->
				      mnesia:delete_object(E)
			      end, Es)
        end,
    mnesia:async_dirty(F).

clean_table_from_bad_node(Node, Host) ->
    F = fun() ->
		Es = mnesia:select(
		       muc_online_room,
		       [{#muc_online_room{pid = '$1',
					  name_host = {'_', Host},
					  _ = '_'},
			 [{'==', {node, '$1'}, Node}],
			 ['$_']}]),
		lists:foreach(fun(E) ->
				      mnesia:delete_object(E)
			      end, Es)
        end,
    mnesia:async_dirty(F).

need_transform({muc_room, {N, H}, _})
  when is_list(N) orelse is_list(H) ->
    ?INFO_MSG("Mnesia table 'muc_room' will be converted to binary", []),
    true;
need_transform({muc_room, {_N, _H}, Opts}) ->
    case lists:keymember(allow_private_messages, 1, Opts) of
        true ->
            ?INFO_MSG("Mnesia table 'muc_room' will be converted to allowpm", []),
            true;
        false ->
            false
    end;

need_transform({muc_registered, {{U, S}, H}, Nick})
  when is_list(U) orelse is_list(S) orelse is_list(H) orelse is_list(Nick) ->
    ?INFO_MSG("Mnesia table 'muc_registered' will be converted to binary", []),
    true;
need_transform(_) ->
    false.

transform({muc_room, {N, H}, Opts} = R)
  when is_list(N) orelse is_list(H) ->
    R#muc_room{name_host = {iolist_to_binary(N), iolist_to_binary(H)},
	       opts = mod_muc:opts_to_binary(Opts)};
transform(#muc_room{opts = Opts} = R) ->
    Opts2 = case lists:keyfind(allow_private_messages, 1, Opts) of
        {_, Value} when is_boolean(Value) ->
            Value2 = case Value of
                         true -> anyone;
                         false -> none
                     end,
            lists:keyreplace(allow_private_messages, 1, Opts, {allowpm, Value2});
        _ ->
            Opts
    end,
    R#muc_room{opts = Opts2};
transform(#muc_registered{us_host = {{U, S}, H}, nick = Nick} = R) ->
    R#muc_registered{us_host = {{iolist_to_binary(U), iolist_to_binary(S)},
				iolist_to_binary(H)},
		     nick = iolist_to_binary(Nick)}.
