%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 聊天监控代理
%%% 收集玩家的聊天信息，并向PHP聊天监控系统发送内容
%%% @end
%%% @since 2017-05-16
%%% -------------------------------------------------------
-module(chat_monitor_agent).
-behaviour(gen_server).
-author("huangyongxing@yeah.net").
%% API
-export([
        start_link/0
        ,role_chat/4
        ,role_chat/5
        ,send_to_adm/3
    ]).
%% gen_server callbacks
-export([init/1,
        handle_call/3,
        handle_cast/2,
        handle_info/2,
        terminate/2,
        code_change/3
    ]).
%% only for debug
-export([
        gm_set_log_state/1
        ,gm_set_ver_issuer/1
        ,gm_set_game_version/2
    ]).

-include("common.hrl").
-include("server.hrl").
-include("chat.hrl").
-include("guild.hrl").
-include("figure.hrl").
-include("game_vsn.hrl").
-include("def_fun.hrl").

-define(SERVER, ?MODULE).

-define(CHECK_TIME, 10000).         % 检测重试及清理超时消息
-define(MAX_TRY_TIMES, 3).          % 重试次数限制

-define(STATE_WAIT_REPONSE, 0).     % 等待响应状态
-define(STATE_GOT_REPONSE, 1).      % 已获得响应状态

-define(CHAT_KEY(RequestId), {chat, RequestId}).
-define(LOG_KEY(Id), {log, Id}).

-define(SAVE_TIME, 60000).          % 写入聊天日志的时间
-define(SAVE_ONCE_NUM, 30).         % 单次写入DB最大数量

%% 开关：修改公会公告是否短信通知管理员 true | false
-define(ANNOUNCE_TO_ADM, false).

%% -------------------------------------------------------
%% 与CP监控服务端phper的约定：
%% name=xms&opt=put&auth=Kiw861HbqsgPoqay27J 这些参数为固定GET方式传输，不能以POST方式传输
%% 其他内容体为单个的JSON数据，直接以POST方式传输（需要对这个JSON数据进行urlencode）
%% POST内容类似：
%% {"source":"zxy","role_id":1,"role_name":"\u7687\u752b\u827a\u8f69",
%% "role_data":{"lv":50,"vip":0,"time":1494940926,"srvid":121},
%% "type":1,"content":"\u5385\u97b4 \u5765**\u6709",
%% "server_db":"jpmx_sy1","ip":"192.168.5.121"}
%%
%% 查询中心存储的数据量信息
%% http://123.206.98.92:1221/?name=xms&opt=status&auth=Kiw861HbqsgPoqay27J
%%
%% 速游后台聊天监控
%% -define(MONITOR_URL_SUYOU, "http://www.test.com:1011/test/test_show_req.php").
-define(MONITOR_URL_SUYOU, "http://mqapi.suyougame.com/chat/mrzj").
-define(SUYOU_SECRET_KEY, "13D3D98D3A51DB4A1411EBFE2C2A31D4").
%% -------------------------------------------------------

%% -------------------------------------------------------
%% 漫灵定义的游戏常量：mId和mKey
-define(MID_MANLING, 20015).
-define(MKEY_MANLING, 40371208).
%% 漫灵方聊天记录服地址（与mId相关，这里省去拼接直接硬编码）
%% http(s)://gamechat.manlinggame.com/Log/Chat/{监控Id(mId)}
-define(MONITOR_URL_MANLING, "http://gamechat.manlinggame.com/Log/Chat/20015").
%% -------------------------------------------------------

-record(state, {
        timer_check = undefined             % 定时器（用于清理多次通知远程monitor失败的数据）
        ,database = <<>>                    % 本地数据库名，用于远程monitor确认所在服
        ,log_to_monitor = false             % 是否记录到远程monitor中
        ,timer_log = undefined              % 数据本地入库定时器
        ,log_id = 0                         % 缓存记录id（用于本地入库前缓存）
        ,game_version = 0                   % 版本
        ,is_debug = false                   % 是否DEBUG版
    }).

-record(chat_info, {
        id = 0                              % 角色id
        ,accname = <<>>                     % 平台账号
        ,name = <<>>                        % 角色名
        ,scene_id = 0                       % 场景id
        ,guild_id = 0                       % 帮派id
        ,guild_name = <<>>                  % 帮派名字
        ,source = <<>>                      % 渠道
        ,level = 0                          % 角色等级
        ,vip = 0                            % VIP等级
        ,sex = 1                            % 性别
        ,power = 0                          % 战力
        ,career = 0                         % 职业
        ,server_id = 0                      % 游戏服配置的server_num
        ,address = <<>>                     % IP地址
        ,chat_to_id = 0                     % 私聊角色id
        ,chat_to_name = <<>>                % 私聊角色名
        ,chat_to_accname = <<>>             % 私聊角色账号
        ,chat_to_level = 0                  % 私聊角色等级
        ,chat_to_vip = 0                    % 私聊角色VIP等级
        ,chat_to_power = 0                  % 私聊角色战力
        ,chat_to_career = 0                 % 私聊角色职业
        ,content = <<>>                     % 发言内容
        ,type = 0                           % 聊天频道类型
        ,time = 0                           % 发言时间
        ,charge_money = 0                   % 累充金额（元）
    }).

%%%===================================================================
%%% API
%%%===================================================================

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% 角色发送聊天内容
role_chat(Player, ChatType, Msg, ChatTime) ->
    role_chat(Player, ChatType, Msg, ChatTime, []).

role_chat(_, _, <<>>, _, _) -> ignore;
role_chat(_, _,  "", _, _) -> ignore;
role_chat(#player_status{
        id = RoleId
        ,accname = AccName
        ,figure = Figure
        ,source = Source
        ,scene = SceneId
        ,server_id = ServerId
        ,ip = Address
    }, ChatType, Msg, ChatTime, OtherChatData) ->
    #chat_to{
        id = ChatToId
        ,name = ChatToName
        ,accname = ChatToAccName
        ,level = ChatToLevel
        ,vip_level = ChatToVipLevel
        ,power = ChatToPower
        ,career = ChatToCareer
    } = get_chat_to(ChatType, OtherChatData),
    #figure{name = RoleName, guild_id = GuildId, guild_name = GuildName, power = Power,
        career = Career, lv = Level, vip = VipLevel, sex = Sex} = Figure,
    ChargeMoney = lib_recharge_data:get_total_rmb(RoleId),
    ChatInfo = #chat_info{
        id = RoleId
        ,accname = util:make_sure_binary(AccName)
        ,name = util:make_sure_binary(RoleName)
        ,scene_id = SceneId
        ,guild_id = GuildId
        ,guild_name = util:make_sure_binary(GuildName)
        ,source = util:make_sure_binary(Source)
        ,level = Level
        ,vip = VipLevel
        ,sex = Sex
        ,power = Power
        ,career = Career
        ,server_id = ServerId
        ,address = util:ip2bin(Address)
        ,content = util:make_sure_binary(Msg)
        ,type = ChatType
        ,time = ChatTime
        ,chat_to_id = ChatToId
        ,chat_to_name = unicode:characters_to_binary(ChatToName)
        ,chat_to_accname = unicode:characters_to_binary(ChatToAccName)
        ,chat_to_level = ChatToLevel
        ,chat_to_vip = ChatToVipLevel
        ,chat_to_career = ChatToCareer
        ,chat_to_power = ChatToPower
        ,charge_money = ChargeMoney
    },
    gen_server:cast(?SERVER, {role_chat, ChatInfo}).

%% 推送通知给管理员（通知接口由外部系统提供，这里只负责发送请求到对应接口）
send_to_adm(RoleId, RoleName, Content) ->
    BinRoleName = util:make_sure_binary(RoleName),
    BinContent  = util:make_sure_binary(Content),
    gen_server:cast(?SERVER, {send_to_adm, RoleId, BinRoleName, BinContent}).

%% GM命令，设置是否向聊天监控系统发出请求
gm_set_log_state(IsLogToMonitor) ->
    gen_server:cast(?SERVER, {log_to_monitor, IsLogToMonitor}).

%% GM命令，设置特定发行商版本，便于测试漫灵版聊天监控
gm_set_ver_issuer(VerIssuer) ->
    gen_server:cast(?SERVER, {ver_issuer, VerIssuer}).

%% GM命令，设置游戏版本，便于测试向管理员推送消息
gm_set_game_version(GameVersion, IsDebug) ->
    gen_server:cast(?SERVER, {game_version, GameVersion, IsDebug}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([]) ->
    process_flag(trap_exit, true),
    Database    = unicode:characters_to_binary(config:get_dbname()),
    GameVersion = data_vsn:get_game_vsn(),
    IsDebug     = config:get_server_debug(),
    NeedLog     = (not IsDebug) andalso (GameVersion =:= ?VER_MAIN),
    VerIssuer   = data_vsn:get_issuer_vsn(),
    erlang:put(ver_issuer, VerIssuer),
    {ok, #state{database = Database, log_to_monitor = NeedLog, game_version = GameVersion, is_debug = IsDebug}}.

handle_call(Request, From, State) ->
    try
        do_handle_call(Request, From, State)
        catch _ErrorClass:_Error ->
            ?ERR("handle_call Request:~w, Error:~w~n~p", [Request, _Error, erlang:get_stacktrace()]),
            {reply, error, State}
    end.

handle_cast(Msg, State) ->
    try
        do_handle_cast(Msg, State)
        catch _ErrorClass:_Error ->
            ?ERR("handle_cast Msg:~w, Error:~w~n~p", [Msg, _Error, erlang:get_stacktrace()]),
            {noreply, State}
    end.

handle_info(Info, State) ->
    try
        do_handle_info(Info, State)
        catch _ErrorClass:_Error ->
            ?ERR("handle_info Info:~w, Error:~w~n~p", [Info, _Error, erlang:get_stacktrace()]),
            {noreply, State}
    end.

terminate(_Reason, _State) ->
    save_log(sync),
    ok.

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

%%%===================================================================
%%% handle_call/handle_cast/handle_info Internal functions
%%%===================================================================

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

do_handle_cast({role_chat, ChatInfo},
        State = #state{
        timer_check = TimerCheck,
        database = Database, log_to_monitor = LogToMonitor,
        timer_log = TimerLog, log_id = LogId
    }) ->
    case LogToMonitor of
        true  ->
            try
                send_to_monitor(Database, ChatInfo)
                catch _ErrClass:_ErrReason ->
                    ?ERR("chat info [~w]~n~w,~w~p", [ChatInfo, _ErrClass, _ErrReason, erlang:get_stacktrace()]),
                    ignore
            end;
        false -> ignore
    end,
    NewTimerCheck =
    case is_reference(TimerCheck) orelse (not LogToMonitor) of
        true  -> TimerCheck;
        false -> erlang:start_timer(?CHECK_TIME, self(), timer_check)
    end,
    NewTimerLog =
    case is_reference(TimerLog) of
        true  -> TimerLog;
        false -> erlang:start_timer(?SAVE_TIME, self(), timer_log)
    end,
    NewLogId = case LogId + 1 > 10000000 of
        true  -> 1;
        false -> LogId + 1
    end,
    log_chat_cache(NewLogId, ChatInfo),
    NewState = State#state{timer_check = NewTimerCheck, timer_log = NewTimerLog, log_id = NewLogId},
    % %% 玩家等级较低时，修改公会宣言时需要检查推送给管理员
    % case ChatType of
    %     ?CHAT_CHANNEL_ANNOUNCE when RoleLevel < 300 andalso ?ANNOUNCE_TO_ADM ->
    %         do_handle_cast({send_to_adm, RoleId, RoleName, Content}, NewState);
    %     _ ->
    %         {noreply, NewState}
    % end;
    {noreply, NewState};

do_handle_cast({send_to_adm, RoleId, RoleName, Content}, State = #state{
        database = Database, game_version = ?VER_MAIN, is_debug = false
    }) ->
    %% 简体服才发送给管理员
    AppId       = 67,
    AppKey      = "Z8OeN87ysiRZvNteFGirbIDrrAqG3x1k",
    ServerNum   = config:get_server_num(),
    NowTime     = utime:unixtime(),
    VarifySign  = util:md5(lists:concat([AppId, NowTime, AppKey])),
    %% PS: 实际上URI为AppId相关，这里省去拼接直接硬编码
    %% URI         = lists:concat(["http://notice.suyougame.com/api/send/", AppId]),
    URI         = "http://notice.suyougame.com/api/send/67",
    PostData    = [
        {<<"check_time">>, NowTime}
        ,{<<"check_sign">>, VarifySign}
        ,{<<"gid">>, 0}
        ,{<<"sdb">>, Database}
        ,{<<"role_id">>, RoleId}
        ,{<<"role_name">>, util:make_sure_binary(RoleName)}
        ,{<<"content">>, util:make_sure_binary(Content)}
        ,{<<"serverNum">>, ServerNum}
    ],
    EncodeParams = mochiweb_util:urlencode(PostData),
    do_request_get(URI, EncodeParams),
    {noreply, State};
do_handle_cast({send_to_adm, _, _, _}, State) ->
    {noreply, State};

%% GM命令：设置是否向聊天监控系统发送消息
do_handle_cast({log_to_monitor, SetValue}, State) ->
    case is_boolean(SetValue) of
        true  ->
            {noreply, State#state{log_to_monitor = SetValue}};
        false ->
            {noreply, State}
    end;

%% GM命令：改变或重设ver_issuer
do_handle_cast({ver_issuer, VerIssuer}, State) ->
    case VerIssuer of
        reset ->    % 重新获取
            RealVerIssuer = data_vsn:get_issuer_vsn(),
            erlang:put(ver_issuer, RealVerIssuer);
        _ ->
            erlang:put(ver_issuer, VerIssuer)
    end,
    {noreply, State};

%% GM命令：设置游戏版本，便于测试
do_handle_cast({game_version, GameVersion, IsDebug}, State) ->
    {NewGameVersion, NewIsDebug} =
    case GameVersion of
        reset ->    % 重新获取
            {data_vsn:get_game_vsn(), config:get_server_debug()};
        _ ->
            {GameVersion, IsDebug}
    end,
    {noreply, State#state{game_version = NewGameVersion, is_debug = NewIsDebug}};

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

do_handle_info({http, ReplyInfo}, State) ->
    http_reply(ReplyInfo),
    {noreply, State};

do_handle_info({timeout, TimerCheck, timer_check}, State = #state{timer_check = TimerCheck}) ->
    CheckRet = check(),
    case CheckRet of
        [] ->
            {noreply, State#state{timer_check = undefined}};
        _ ->
            NewTimerCheck = erlang:start_timer(?CHECK_TIME, self(), timer_check),
            {noreply, State#state{timer_check = NewTimerCheck}}
    end;

do_handle_info({timeout, TimerLog, timer_log}, State = #state{timer_log = TimerLog}) ->
    save_log(async),
    {noreply, State#state{timer_log = undefined}};

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

%%%===================================================================
%%% Internal functions
%%%===================================================================

gen_ext_data(#chat_info{}) ->
    {<<>>, <<>>}.

send_to_monitor(Database, ChatInfo = #chat_info{time = ChatTime}) ->
    [
        begin
                EncodeParams =
                case UrlId of
                    suyou   -> pack_req_suyou(Database, ChatInfo);
                    manling -> pack_req_manling(ChatInfo);
                    _       -> false
                end,
                case EncodeParams of
                    false -> ignore;
                    _ ->
                        %% ?DEBUG_TTY("ver:~w, encode params: ~ts", [UrlId, EncodeParams]),
                        RequestId = do_request(Url, EncodeParams),
                        %% ?DEBUG_TTY("RequestId : ~w", [RequestId]),
                        case RequestId =/= error of
                            true  -> put_request_info(RequestId, UrlId, 1, ?STATE_WAIT_REPONSE, ChatTime, EncodeParams);
                            false -> ignore
                        end
                end
        end || {UrlId, Url} <- get_url_list()
    ],
    ok.

%% 处理http回应
% {http, ReplyInfo}, ReplyInfo:
% {RequestId, saved_to_file}
% {RequestId, {error, Reason}}
% {RequestId, Result}
% {RequestId, stream_start, Headers}
% {RequestId, stream_start, Headers, HandlerPid}
% {RequestId, stream, BinBodyPart}
% {RequestId, stream_end, Headers}
http_reply({RequestId, saved_to_file}) ->
    erase_request_info(RequestId),
    ignore;
http_reply({RequestId, {error, _Reason}}) ->
    % ?INFO_TTY("error: ~p", [_Reason]),
    case erlang:get(?CHAT_KEY(RequestId)) of
        undefined ->
            ignore;
        {UrlId, TryTimes, _State, ChatTime, EncodeParams} when TryTimes < ?MAX_TRY_TIMES ->
            put_request_info(RequestId, UrlId, TryTimes, ?STATE_GOT_REPONSE, ChatTime, EncodeParams),
            ignore;
        _ ->
            erase_request_info(RequestId)
    end;
http_reply({RequestId, _Result}) ->
    % ?INFO_TTY("\nget RequestId [~w]\nResult:~tp", [RequestId, _Result]),
    %% 成功返回(有可能是返回失败的HTTP CODE，忽略结果)
    erase_request_info(RequestId),
    ok;
http_reply({RequestId, _, _}) ->
    erase_request_info(RequestId),
    ignore;
http_reply({RequestId, _, _, _}) ->
    erase_request_info(RequestId),
    ignore;
http_reply(_) ->
    ignore.

put_request_info(RequestId, UrlId, TryTimes, State, ChatTime, EncodeParams) ->
    erlang:put(?CHAT_KEY(RequestId), {UrlId, TryTimes, State, ChatTime, EncodeParams}).

erase_request_info(RequestId) ->
    erlang:erase(?CHAT_KEY(RequestId)).

check() ->
    Keys = erlang:get_keys(),
    do_check(Keys, []).

do_check([Key = ?CHAT_KEY(OldReuestId) | Keys], AccList) ->
    case erlang:get(Key) of
        {UrlId, TryTimes, State, ChatTime, EncodeParams} ->
            if
                TryTimes > ?MAX_TRY_TIMES ->
                    %% ?DEBUG_TTY("timeout clean [~w], max times:", [ChatTime]),
                    erase_request_info(OldReuestId),
                    do_check(Keys, AccList);
                State =:= ?STATE_WAIT_REPONSE ->
                    %% ?DEBUG_TTY("requesting [~w], ignore", [Key]),
                    do_check(Keys, [Key | AccList]);
                State =:= ?STATE_GOT_REPONSE ->
                    NewKey =
                    try
                        case find_url(UrlId) of
                            false ->
                                %% ?DEBUG_TTY("Invalid url id:~w", [UrlId]),
                                undefined;
                            Url   ->
                                RequestId = do_request(Url, EncodeParams),
                                %% ?DEBUG_TTY("retry : [~w=>~w]", [Key, ?CHAT_KEY(RequestId)]),
                                case RequestId =/= error of
                                    true  ->
                                        case TryTimes >= ?MAX_TRY_TIMES of
                                            true  ->
                                                undefined;
                                            false ->
                                                put_request_info(RequestId,
                                                    UrlId,
                                                    TryTimes + 1,
                                                    ?STATE_WAIT_REPONSE,
                                                    ChatTime, EncodeParams
                                                ),
                                                ?CHAT_KEY(RequestId)
                                        end;
                                    false ->
                                        undefined
                                end
                        end
                        catch _:_ ->
                            ?ERR("do_check [~s] : ~p", [EncodeParams, erlang:get_stacktrace()]),
                            undefined
                    end,
                    erase_request_info(OldReuestId),
                    do_check(Keys, [NewKey | AccList]);
                true ->
                    %% ?DEBUG_TTY("expection: ~w", [Key]),
                    erase_request_info(OldReuestId),
                    do_check(Keys, AccList)
            end;
        _ ->
            erase_request_info(OldReuestId),
            do_check(Keys, AccList)
    end;
do_check([_ | Keys], AccList) ->
    do_check(Keys, AccList);
do_check([], AccList) ->
    AccList.

do_request(Url, EncodeParams) ->
    % ?INFO_TTY("do request, url:~ts, params:~ts", [Url, EncodeParams]),
    case httpc:request(post,
            {Url, [], "application/x-www-form-urlencoded", EncodeParams},
            [{timeout, 10000}],
            [{sync, false}]) of
        {ok, RequestId} ->
            RequestId;
        {error, Reason} ->
            ?ERR("do httpc request error: ~w", [Reason]),
            error
    end.

do_request_get(Url, EncodeParams) when is_binary(EncodeParams) ->
    do_request_get(Url, unicode:characters_to_list(EncodeParams));
do_request_get(Url, EncodeParams) ->
    FullUrl = lists:concat([Url, "?", EncodeParams]),
    do_request(FullUrl, <<>>).

find_url(UrlId) ->
    case lists:keyfind(UrlId, 1, get_url_list()) of
        {UrlId, Url} -> Url;
        _ -> false
    end.

%% 聊天监控web服务器相关URL列表
%% get_url_list() -> [ {UrlId :: atom(), Url :: string:string()} ].
get_url_list() ->
    case get(ver_issuer) of
        ?VER_ISSUER_ML ->
            [
                {suyou, ?MONITOR_URL_SUYOU}
                ,{manling, ?MONITOR_URL_MANLING}
            ];
        _ ->
            [
                {suyou, ?MONITOR_URL_SUYOU}
            ]
    end.

get_chat_to(?CHAT_CHANNEL_PRIVATE, ChatTo = #chat_to{}) ->
    ChatTo;
get_chat_to(_, _) ->
    #chat_to{id = 0, name = <<>>, accname = <<>>}.

%% -------------------------------------------------------
%% 速游聊天监控请求数据封装
%% -------------------------------------------------------

pack_req_suyou(Database, ChatInfo) ->
    #chat_info{
        id = RoleId
        ,accname = AccName
        ,name = RoleName
        ,source = Source
        ,level = RoleLevel
        ,vip = VipLevel
        ,power = Power
        ,career = Career
        ,address = Address
        ,type = ChatType
        ,content = Content
        ,chat_to_id = ChatToId
        ,chat_to_accname = ChatToAccName
        ,chat_to_name = ChatToName
        ,chat_to_level = ChatToLv
        ,chat_to_vip = ChatToVip
        ,chat_to_power = ChatToPower
        ,chat_to_career = ChatToCareer
    } = ChatInfo,
    Time = utime:unixtime(),
    Sign = util:md5(lists:concat([Time, ?SUYOU_SECRET_KEY])),
    {ExtList1, ExtList2}  = gen_ext_data(ChatInfo),
    Date = get_date(),
    RegServerId = proc_reg_server_id(),
    RegServerInfo =
    case RegServerId of
        undefined -> [];
        _ -> [{<<"regServerId">>, RegServerId}]
    end,
    RoleData = RegServerInfo ++ [
        {<<"channel">>, ChatType}
        ,{<<"dbName">>, Database}
        ,{<<"source">>, Source}
        ,{<<"content">>, Content}
        ,{<<"fromUserId">>, AccName}
        ,{<<"fromRoleId">>, RoleId}
        ,{<<"fromRoleName">>, RoleName}
        ,{<<"fromLevel">>, RoleLevel}
        ,{<<"fromVip">>, VipLevel}
        ,{<<"fromPower">>, Power}
        ,{<<"fromCareer">>, Career}
        ,{<<"fromRoleData">>, ExtList1}
        ,{<<"toUserId">>, ChatToAccName}
        ,{<<"toRoleId">>, ChatToId}
        ,{<<"toRoleName">>, ChatToName}
        ,{<<"toLevel">>, ChatToLv}
        ,{<<"toVip">>, ChatToVip}
        ,{<<"toPower">>, ChatToPower}
        ,{<<"toCareer">>, ChatToCareer}
        ,{<<"toRoleData">>, ExtList2}
        ,{<<"date">>, util:make_sure_binary(Date)}
        ,{<<"time">>, Time}
        ,{<<"ip">>, Address}
        ,{<<"sign">>, util:make_sure_binary(Sign)}
    ],
    iolist_to_binary(mochiweb_util:urlencode(RoleData)).

%% -------------------------------------------------------
%% 漫灵聊天监控请求数据封装
%% -------------------------------------------------------

pack_req_manling(ChatInfo = #chat_info{
        id = RoleId
        ,accname = AccName
        ,name = RoleName
        ,level = RoleLevel
        ,vip = VipLevel
        % ,server_id = ServerId
        ,address = Address
        ,content = Content
        ,time = ChatTime
        ,charge_money = ChargeMoney
    }) ->
    %% mid和mKey是漫灵定义的游戏常量
    VCode       = ?MKEY_MANLING bxor (ChatTime div 1000),
    ChatToStr   = pack_chat_to_manling(ChatInfo),
    ServerInfo  = get_server_info(),
    PostData = ServerInfo ++ [
        {<<"vcode">>, VCode}                % 验证码 vcode = mKey^(chatTime%1000000)，ChatTime是毫秒级
        % ,{<<"debug">>, 1}                 % 1为调试模式，缺省为0
        ,{<<"userId">>, AccName}            % 漫灵用户标识
        ,{<<"roleId">>, RoleId}
        ,{<<"roleName">>, RoleName}
        % ,{<<"serverName">>, <<>>}         % 服务器名，缺
        ,{<<"level">>, RoleLevel}           % 角色等级
        ,{<<"vipLevel">>, VipLevel}         % VIP等级
        ,{<<"recharge">>, ChargeMoney}      % 累计充值金额（单位：元）
        ,{<<"chatTime">>, ChatTime * 1000}  % 发言时间（毫秒）
        ,{<<"chatTo">>, ChatToStr}          % 聊天频道对象
        ,{<<"ip">>, Address}
        ,{<<"content">>, Content}
    ],
    iolist_to_binary(mochiweb_util:urlencode(PostData)).

%% 漫灵版chatTo信息
pack_chat_to_manling(#chat_info{
        type = ChatType
        ,scene_id = SceneId
        ,guild_id = GuildId
        ,guild_name = GuildName
        ,chat_to_id = ChatToId
        ,chat_to_name = ChatToName
    }) ->
    if
        ChatType =:= ?CHAT_CHANNEL_SCENE ->
            util:fmt_bin(<<"场景_~w"/utf8>>, [SceneId]);
        ChatType =:= ?CHAT_CHANNEL_GUILD ->
            util:fmt_bin(<<"公会_~w_~ts"/utf8>>, [GuildId, GuildName]);
        ChatType =:= ?CHAT_CHANNEL_PRIVATE ->
            util:fmt_bin(<<"私聊_~w_~ts"/utf8>>, [ChatToId, ChatToName]);
        % ChatType =:= ?CHAT_CHANNEL_ANNOUNCE ->
        %     util:fmt_bin(<<"公会宣言_~w_~ts"/utf8>>, [GuildId, GuildName]);
        ChatType =:= ?CHAT_CHANNEL_WORLD ->
            <<"世界"/utf8>>;
        true ->
            <<"其它"/utf8>>
    end.

get_server_info() ->
    %% 服务器对外的ServerId（注册系统服号）
    RegServerId = proc_reg_server_id(),
    case RegServerId of
        undefined -> [];
        _ -> [ {<<"serverId">>, RegServerId} ]
    end.

proc_reg_server_id() ->
    case get(reg_server_id) of
        Id when is_integer(Id) ->
            Id;
        _ ->
            case config:get_reg_server_id() of
                undefined ->
                    undefined;
                Id ->
                    put(reg_server_id, Id),
                    Id
            end
    end.

%% eg: 2021.04.01 10:2:3 => "20210401100203"
get_date() ->
    {{Y,M,D},{H,Min,S}} = utime:localtime(),
    M2 = ?IF(M > 9, M, [0, M]),
    D2 = ?IF(D > 9, D, [0, D]),
    H2 = ?IF(H > 9, H, [0, H]),
    Min2 = ?IF(Min > 9, Min, [0, Min]),
    S2 = ?IF(S > 9, S, [0, S]),
    lists:concat(lists:flatten([Y,M2,D2,H2,Min2,S2])).

%% -------------------------------------------------------
%% 数据库日志存储
%% -------------------------------------------------------

%% 记录聊天数据到缓存，等待定期写入数据库
log_chat_cache(LogId, ChatInfo) ->
    erlang:put(?LOG_KEY(LogId), ChatInfo).

%% Sync :: sync | async
save_log(Sync) ->
    Keys = erlang:get_keys(),
    check_save_log(Sync, Keys, []).

%% 清除掉进程字典数据，同时收集起来统一保存
check_save_log(Sync, [Key = {log, _Id} | Keys], LogChatList) ->
    ChatInfo = erlang:erase(Key),
    case ChatInfo of
        #chat_info{} ->
            check_save_log(Sync, Keys, [ChatInfo | LogChatList]);
        _ ->
            check_save_log(Sync, Keys, LogChatList)
    end;
check_save_log(Sync, [_Key | Keys], LogChatList) ->
    check_save_log(Sync, Keys, LogChatList);
check_save_log(Sync, [], LogChatList) ->
    case Sync of
        sync  -> do_save_log(LogChatList, 0);
        async -> spawn_link(fun() -> do_save_log(LogChatList, 50) end)
    end.

do_save_log([], _SleepTime) -> ok;
do_save_log(DataList, SleepTime) ->
    {PreList, TailList} = ulists:split(?SAVE_ONCE_NUM, DataList),
    try
        do_save_log_data(PreList)
    catch
        ?EXCEPTION(_Class, _Reason, _Stacktrace) ->
            ?ERR("Save log_chat error:~w,~n  ~p", [{_Class, _Reason}, ?GET_STACK(_Stacktrace)])
    end,
    case is_integer(SleepTime) andalso SleepTime >= 10 andalso SleepTime =< 2000 of
        true  -> timer:sleep(SleepTime);
        false -> ignore
    end,
    do_save_log(TailList, SleepTime).

do_save_log_data(DataList) ->
    DataString = util:join_bitstring([fmt_data_string(Data) || Data <- DataList, is_record(Data, chat_info)], <<",">>),
    case DataString =:= <<>> of
        true  -> ok;
        false ->
            Sql = util:fmt_bin("INSERT INTO log_chat (`role_id`,`name`,`level`,`vip_level`,`address`,`type`,`scene_id`,`guild_id`,`guild_name`,`chat_to_id`,`chat_to_name`,`content`,`ctime`) VALUES ~ts", [DataString]),
            % ?INFO("sql:~ts", [Sql]),
            db:execute(Sql)
    end.

fmt_data_string(#chat_info{
        id = Id, name = Name, scene_id = SceneId, level = Level, vip = VipLevel, address = Address,
        guild_id = GuildId, guild_name = GuildName, chat_to_id = ChatToId, chat_to_name = ChatToName,
        content = Content, type = ChatType, time = ChatTime
    }) ->
    util:fmt_bin("(~w,~ts,~w,~w,'~s',~w,~w,~w,~ts,~w,~ts,~ts,~w)",
        [Id, mysql:encode(Name), Level, VipLevel, Address, ChatType, SceneId,
            GuildId, mysql:encode(GuildName), ChatToId,
            mysql:encode(ChatToName), mysql:encode(Content), ChatTime
        ]).

