%% -----------------------------------------------------
%% @author  :ljy
%% moudle   :mod_mail.erl
%% desc     :邮件管理
%% time     :2021.01.07
%% ------------------------------------------------------
-module(mod_mail).

-behaviour(gen_server).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 收集所有邮件请求，间隔进行批量写入，30条一批，
%% 每次处理完所有邮件，只有新的请求过来才会触发定时器。
%% 重要的邮件，直接入库。入库失败10s后重试
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


%% API
-export([
    start_link/0
    , schedule_send_mail/1    %% 立即入库
    , direct_send_mail/1      %% 按规则统一定时入库
]).

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

-include("common.hrl").
-include("mail.hrl").
-include("server.hrl").
-include("errcode.hrl").
-include("def_id_create.hrl").
-include("predefine.hrl").
-include("def_fun.hrl").

%% 寻常入库规则30封/3s
-define(SAVE_TIME, 3000).           % 间隔入库时间
-define(SAVE_ONCE_NUM, 30).         % 单次写入DB最大数量
%% 当邮件规模达到600时，加快入库频率
-define(MAX_MAIL_NUM, 600).
-define(URGENT_SAVE_TIME, 2000).    % 间隔入库时间
-define(URGENT_SAVE_ONCE_NUM, 50).  % 单次写入DB最大数量

-define(MAIL_KEY, mail).
-define(AGAIN_KEY, again_save).

-define(CHECK_TIME, 10000).         % 入库失败重试间隔时间

-record(state, {
    timer_ref = undefined               % 数据本地入库定时器
    , mail_id = 0                       % 记录id
    , queue
    , mail_num = 0                      % 邮件数量
}).


%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
    SpawnOpts = [
        {spawn_opt, [{fullsweep_after, 128}]}
    ],
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], SpawnOpts).

%% 立即入库
direct_send_mail(MailInfo) ->
    gen_server:cast(?MODULE, {direct_send_mail, MailInfo}).

%% 按规则统一定时入库
schedule_send_mail(MailInfo) ->
    gen_server:cast(?MODULE, {schedule_send_mail, MailInfo}).

%% 邮件分批入库
batch_save_mail([]) ->
    ok;
batch_save_mail(DataList) ->
    {PreList, TailList} = ulists:split(?SAVE_ONCE_NUM, DataList),
    do_save_mail(PreList),
    batch_save_mail(TailList).

do_save_mail([]) ->
    ok;
do_save_mail(DataList) ->
    Ref =
        try
            do_save_mail_data(DataList)
        catch ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace) ->
            case get(?AGAIN_KEY) of
                List when is_list(List) -> put(?AGAIN_KEY, DataList ++ List);
                _ -> put(?AGAIN_KEY, DataList)
            end,
            erlang:send_after(?CHECK_TIME, self(), again_save),
            ?ERR("save mail error: ~w", [{_ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}]),
            false
        end,
    if
        Ref =/= false ->
            F = fun([Id, Type, State, Locked, _, RoleId, Title, _, ModuleId, SubModuleId, Timestamp, Time, _, SendLogType, CmAttachment, Important]) ->
                Mail = make_mail([Id, Type, State, Locked, 0, RoleId, Title, CmAttachment, Timestamp, Time, ModuleId, SubModuleId, Important]),
                lib_mail:log_mail_send(SendLogType, Mail),
                lib_player:apply_cast(RoleId, ?APPLY_CAST_STATUS, lib_mail, add_mail, [[Mail]])
                end,
            lists:foreach(F, DataList);
        true -> skip
    end.

do_save_mail_data(DataList) ->
    DataString = util:join_bitstring([fmt_data_string(Data) || Data <- DataList], <<",">>),
    case DataString =:= <<>> of
        true -> ok;
        false ->
            Sql = util:fmt_bin("INSERT INTO mail(`id`, `type`, `state`, `locked`, `sid`, `rid`, `title`, `cm_attachment`,
            `module_id`, `sub_module_id`, `timestamp`, `effect_st`, `content`, `important`) VALUES ~ts", [DataString]),
            db:execute(Sql)
    end.

fmt_data_string([Id, Type, State, Locked, SId, RId, Title, CmAttachmentBin, ModuleId, SubModuleId, Timestamp, Time, Content, _SendLogType, _CmAttachment, Important]) ->
    util:fmt_bin("(~w, ~w, ~w, ~w, ~w, ~w, '~ts', '~ts', ~w, ~w, ~w, ~w, '~ts', ~w)",
        [Id, Type, State, Locked, SId, RId, Title, CmAttachmentBin, ModuleId, SubModuleId, Timestamp, Time, Content, Important]).

make_mail([Id, Type, State, Locked, SId, RoleId, Title, CmAttachment, Timestamp, Time, ModuleId, SubModuleId, Important]) ->
    Mail = #mail{
        id = Id
        , type = Type
        , state = State
        , locked = Locked
        , sid = SId
        , rid = RoleId
        , title = Title
        , content = <<>>
        , cm_attachment = CmAttachment
        , timestamp = Timestamp
        , time = Time
        , effect_st = Time
        , effect_et = lib_mail:get_effect_et(CmAttachment, State, Time)
        , module_id = ModuleId
        , sub_module_id = SubModuleId
        , important = Important
    },
    Mail.

send_all_mail(Queue) ->
    {_SaveNum, MailList, NewQueue} = get_mail_for_queue(?URGENT_SAVE_ONCE_NUM, [], Queue),
    case queue:is_empty(NewQueue) of
        true ->
            do_save_mail(MailList);
        false ->
            send_all_mail(NewQueue)
    end.

get_mail_for_queue(0, MailList, Queue) ->
    {0, lists:reverse(MailList), Queue};
get_mail_for_queue(Count, MailList, Queue) ->
    case queue:out(Queue) of
        {{value, MailInfo}, NewQueue} ->
            get_mail_for_queue(Count - 1, [MailInfo | MailList], NewQueue);
        {empty, NewQueue} ->
            {Count, lists:reverse(MailList), NewQueue}
    end.

save_mail_id(MaidId) ->
    <<_:16, MaxId:32>> = <<MaidId:48>>,
    case catch db:execute(io_lib:format(<<"replace into auto_id set type=~w, count=~w">>, [?MAIL_ID_CREATE, MaxId])) of
        {'EXIT', _} = Reason -> ?ERR("save_mail_id error = ~p", [Reason]);
        _ -> skip
    end.


%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([]) ->
    process_flag(trap_exit, true),
    AutoId = case db:get_row(io_lib:format(<<"select count from auto_id where type=~w">>, [?MAIL_ID_CREATE])) of
                 [TmpAutoId | _] when is_integer(TmpAutoId) -> TmpAutoId;
                 _ -> 0
             end,
    SerId = config:get_server_id(),
    <<AutoId2:48>> = <<SerId:16, AutoId:32>>,
    MailId = case db:get_row(io_lib:format(<<"select max(id) from mail">>, [])) of
                 [Id | _] when is_integer(Id) ->
                     max(Id, AutoId2);
                 _Other ->
                     AutoId2
             end,
    InitQueue = queue:new(),
    {ok, #state{mail_id = MailId, queue = InitQueue}}.

handle_call(Request, From, State) ->
    try
        do_handle_call(Request, From, State)
    catch ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace) ->
        ?ERR("handle_call Request:~w, Error:~w", [Request, {_ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}]),
        {reply, error, State}
    end.

handle_cast(Msg, State) ->
    try
        do_handle_cast(Msg, State)
    catch ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace) ->
        ?ERR("handle_cast Msg:~w, Error:~w", [Msg, {_ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}]),
        {noreply, State}
    end.

handle_info(Info, State) ->
    try
        do_handle_info(Info, State)
    catch ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace) ->
        ?ERR("handle_info Info:~w, Error:~w", [Info, {_ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}]),
        {noreply, State}
    end.

terminate(_Reason, State) ->
    #state{mail_id = MailId, queue = Queue} = State,
    send_all_mail(Queue),
    save_mail_id(MailId),
    ok.

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

do_handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

do_handle_cast({direct_send_mail, [RoleIdList, SendLogType, Type, Title, Content, CmAttachment, ModuleId, SubModuleId, Important]}, State) ->
    #state{mail_id = MailId} = State,
    CmAttachmentBin = util:term_to_bitstring(CmAttachment),
    MailState = ?MAIL_STATE_NO_READ,
    Locked = ?MAIL_LOCKED_NO,
    Timestamp = utime:unixtime(us),
    Time = utime:microsec_to_sec(Timestamp),
    F = fun(RoleId, {Id, MailInfoList}) ->
        MailInfo = [Id, Type, MailState, Locked, 0, RoleId, Title, CmAttachmentBin, ModuleId, SubModuleId, Timestamp, Time, Content, SendLogType, CmAttachment, Important],
        {Id + 1, [MailInfo | MailInfoList]}
        end,
    {NewMailId, MailList} = lists:foldl(F, {MailId, []}, RoleIdList),
    save_mail_id(NewMailId),
    batch_save_mail(MailList),
    NewState = State#state{mail_id = NewMailId},
    {noreply, NewState};

do_handle_cast({schedule_send_mail, [RoleIdList, SendLogType, Type, Title, Content, CmAttachment, ModuleId, SubModuleId, Important]}, State) ->
    #state{timer_ref = Ref, mail_id = MailId, queue = Queue, mail_num = MailNum} = State,
    CmAttachmentBin = util:term_to_bitstring(CmAttachment),
    MailState = ?MAIL_STATE_NO_READ,
    Locked = ?MAIL_LOCKED_NO,
    Timestamp = utime:unixtime(us),
    Time = utime:microsec_to_sec(Timestamp),
    F = fun(RoleId, {Id, MailQueue, Num}) ->
        MailInfo = [Id, Type, MailState, Locked, 0, RoleId, Title, CmAttachmentBin, ModuleId, SubModuleId, Timestamp, Time, Content, SendLogType, CmAttachment, Important],
        {Id + 1, queue:in(MailInfo, MailQueue), Num + 1}
        end,
    {NewMailId, NewQueue, NewMailNum} = lists:foldl(F, {MailId, Queue, MailNum}, RoleIdList),
    if
        is_reference(Ref) -> NewState = State#state{mail_id = NewMailId, queue = NewQueue, mail_num = NewMailNum};
        true ->
            SaveTime = ?IF(NewMailNum >= ?MAX_MAIL_NUM, ?URGENT_SAVE_TIME, ?SAVE_TIME),
            NewRef = erlang:send_after(SaveTime, self(), timer_save),
            NewState = State#state{timer_ref = NewRef, mail_id = NewMailId, queue = NewQueue, mail_num = NewMailNum}
    end,
    {noreply, NewState};

do_handle_cast(_Msg, State) ->
    {noreply, State}.

%% 定时保存
do_handle_info(timer_save, State) ->
    #state{mail_id = MailId, queue = Queue, mail_num = MailNum} = State,
    MaxSaveNum = ?IF(MailNum >= ?MAX_MAIL_NUM, ?URGENT_SAVE_ONCE_NUM, ?SAVE_ONCE_NUM),
    {SaveNum, MailList, NewQueue} = get_mail_for_queue(MaxSaveNum, [], Queue),
    do_save_mail(MailList),
    case queue:is_empty(NewQueue) of
        true ->
            NewMailNum = 0,
            NewRef = undefined,
            save_mail_id(MailId);
        false ->
            NewMailNum = MailNum - (MaxSaveNum - SaveNum),
            SaveTime = ?IF(NewMailNum >= ?MAX_MAIL_NUM, ?URGENT_SAVE_TIME, ?SAVE_TIME),
            NewRef = erlang:send_after(SaveTime, self(), timer_save)
    end,
    {noreply, State#state{timer_ref = NewRef, queue = NewQueue, mail_num = NewMailNum}};

%% 再次尝试保存
do_handle_info(again_save, State) ->
    case erlang:erase(?AGAIN_KEY) of
        DataList when is_list(DataList) ->
            F = fun
                    ([Id, Type, MailState, Locked, SId, RId, Title, CmAttachmentBin, ModuleId, SubModuleId, _Timestamp, _Time, Content, SendLogType, CmAttachment, Important]) ->
                        Res =
                            try
                                insert_mail_to_db_no_transaction(Id, Type, MailState, Locked, SId, RId, Title, CmAttachmentBin, CmAttachment, ModuleId, SubModuleId, Content, Important)
                            catch ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace)  ->
                                ?ERR("Error:~w Args:~w", [{_ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}, [RId, Title, Content, CmAttachment]]),
                                fail
                            end,
                        case Res of
                            {ok, Mail} ->
                                lib_mail:log_mail_send(SendLogType, Mail),
                                lib_player:apply_cast(RId, ?APPLY_CAST_STATUS, lib_mail, add_mail, [[Mail]]),
                                ok;
                            _ ->
                                skip
                        end;
                    (_) -> skip
                end,
            F2 = fun(DataInfo, Counter) ->
                F(DataInfo),
                case Counter < 20 of
                    true -> Counter + 1;
                    false -> timer:sleep(50), 1
                end
                 end,
            spawn(fun() -> lists:foldl(F2, 1, DataList) end),
            ok;
        _ -> skip
    end,
    {noreply, State};

do_handle_info(_Info, State) ->
    {noreply, State}.

%% 插入邮件
%% @return {ok, #mail{}}
insert_mail_to_db_no_transaction(Id, Type, State, Locked, SId, RId, Title, CmAttachmentBin, CmAttachment, ModuleId, SubModuleId, Content, Important) ->
    Timestamp = utime:unixtime(us),
    Time = utime:microsec_to_sec(Timestamp),
    Sql1 = io_lib:format(?sql_mail_insert,
        [Id, Type, State, Locked, SId, RId, Title, CmAttachmentBin, ModuleId, SubModuleId, Timestamp, Time, Content, Important]),
    db:execute(Sql1),
    Mail = make_mail([Id, Type, State, Locked, SId, RId, Title, CmAttachment, Timestamp, Time, ModuleId, SubModuleId, Important]),
    {ok, Mail}.

