%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 回归返利
%%% 从封测服导出数据（或由运营给定数据，加工制作成数据库SQL脚本），
%%% 再导入数据到跨服中心的数据库表，以此作为回归返利依据
%%%
%%% 脚本导出数据使用接口export_sql/0，如果运营提供，参考以下做法：
%%% . 修改comeback_history结构，添加需要的字段
%%% . 修改merge_data/2接口，处理好新增字段的合并或更新
%%% . 将运营提供的excel表，整理成所需要的#comeback_data.data数据，
%%%   即类似[{role_level,10},{charge_gold,1000}]的key-value列表数据，
%%%   并制作成sql脚本（不重要的数据可为默认空值，例如角色id、渠道），
%%%   用于正式开服时导入到跨服数据库中
%%% . 关键的一步，按运营指定的规则，修改奖励发放规则，可能涉及：
%%%   通知等级及发放等级： ?NOTICE_LEVEL、?RETURN_LEVEL
%%%   奖励计算 calc_awards 接口
%%%   发放邮件内容 do_give_awards 接口
%%% @end
%%% @since 2018-12-10
%%% -------------------------------------------------------
-module(comeback).
-author('huangyongxing@yeah.net').
-export([
        init_load_data/0
        ,reload_data/0
        ,handle_event/2
        ,export_sql/0
        ,change_case/2
        ,cross_check_return/2
        ,check_return_callback/4
    ]).
%% 角色回调发放奖励、通知、更新数据等操作
-export([
        do_give_awards/2
        ,do_set_comeback_state/3
    ]).
%% for test
-export([
        get_comeback_from_db/3
        ,gm_set_role_state/1
    ]).
-include("server.hrl").
-include("common.hrl").
-include("game_vsn.hrl").
-include("figure.hrl").
-include("predefine.hrl").
-include("def_event.hrl").
-include("rec_event.hrl").
-include("def_goods.hrl").
-include("def_module.hrl").
%% 测试服测试时的数据，用特定接口导出后，导入到正式服库表使用
%% 使用时，根据测试数据生成 #comeback_history{}
-record(comeback_history, {
        role_level = 0                          % 账号角色最高等级
        ,charge_gold = 0                        % 充值元宝数
        ,login_days = 0                         % 累登天数
    }).

%% 缓存的账户数据
-record(comeback_cache, {
        account = <<>>,                         % 小写的账户名
        list = []                               % [#comeback_data{}]
    }).

-record(comeback_data, {
        data_id = 0                             % 数据库表id
        ,state = 0                              % 数据状态
        ,source = <<>>                          % 渠道
        ,data = []                              % 封测数据信息(二进制数据，使用时再转换)
        ,now_role_id = 0                        % 当前角色id
    }).

%% 功能是否开启（开服到一定阶段就可以关闭了，true表示开放）
-define(FUNC_OPEN, false).

%% 是否处理过回归数据的状态
-define(COMEBACK_STATE_UNDO, 0).                % 未处理过
-define(COMEBACK_STATE_GOT, 1).                 % 本角色已经取得
-define(COMEBACK_STATE_OTHER_GOT, 2).           % 其他角色取得
-define(COMEBACK_STATE_NOT_BACK, 3).            % 非回归角色
-define(COMEBACK_STATE_NOTIFIED, 4).            % 已通知玩家封测信息，但尚未取得奖励

%% 数据是否已使用过
-define(DATA_STATE_UNUSE, 0).                   % 数据未使用
-define(DATA_STATE_USED,  1).                   % 数据已使用

%% 提示信息时的等级（应小于返利等级）
-define(NOTICE_LEVEL, 10).
%% 返利时的等级
-define(RETURN_LEVEL, 80).

%% 封测回归账户数据ETS表名
-define(COMEBACK_TAB, comeback).

%% 导出时充值额度限制，按订单元宝计
%% （0表示所有玩家，1表示至少充值1元宝）
-define(EXPORT_CHARGE, 0).

%% 处理登录后异步事件
handle_event(Player, #event_callback{type_id = ?EVENT_LOGIN_CAST}) ->
    % concurrency test: [spawn(fun() -> check_return(Player) end) || _X <- lists:seq(1, 5)],
    ?FUNC_OPEN andalso check_return(Player),
    {ok, Player};
%% 处理升级事件
handle_event(Player, #event_callback{type_id = ?EVENT_LV_UP}) ->
    % concurrency test: [spawn(fun() -> check_return(Player) end) || _X <- lists:seq(1, 5)],
    ?FUNC_OPEN andalso check_return(Player),
    {ok, Player};
handle_event(Player, _) ->
    {ok, Player}.

%% 检测进行返利或通知封测返利条件
check_return(#player_status{id = Id, accname = Account,
        pid = Pid, source = Source, server_id = ServerId,
        figure = #figure{lv = Level, name = _Name},
        comeback_state = OldState
    }) when Level >= ?RETURN_LEVEL ->
    % ?INFO("check return get: [~w,~ts]", [Id, Account]),
    case lists:member(OldState, [?COMEBACK_STATE_UNDO, ?COMEBACK_STATE_NOTIFIED]) of
        true  ->
            case data_vsn:get_game_vsn() of
                ?VER_MAIN ->
                    %% 检测返利
                    %% 需要模拟并发多个请求进行测试
                    % ?INFO("check return get: [~w,~ts], OldState:~w", [Id, Account, OldState]),
                    clusters_node:apply_cast(?MODULE, cross_check_return,
                        [check_get, {ServerId, Id, Account, Source}]
                    );
                _ ->
                    lib_player:apply_cast(Pid, ?APPLY_CAST_STATUS,
                        ?MODULE, do_set_comeback_state, [?COMEBACK_STATE_NOT_BACK, undefined]
                    )
            end;
        false -> ignore
    end;
check_return(#player_status{id = Id, accname = Account,
        pid = Pid, source = Source, server_id = ServerId,
        figure = #figure{lv = Level, name = _Name},
        comeback_state = ?COMEBACK_STATE_UNDO
    }) when Level >= ?NOTICE_LEVEL ->
    case data_vsn:get_game_vsn() of
        ?VER_MAIN ->
            % ?INFO("check return notice: [~w,~ts]", [Id, Account]),
            %% 检测通知
            clusters_node:apply_cast(?MODULE, cross_check_return,
                [check_notice, {ServerId, Id, Account, Source}]
            );
        _ ->
            lib_player:apply_cast(Pid, ?APPLY_CAST_STATUS,
                ?MODULE, do_set_comeback_state, [?COMEBACK_STATE_NOT_BACK, undefined]
            )
    end;
check_return(_) ->
    ignore.

%% 跨服中心触发检查
cross_check_return(CheckType, {ServerId, Id, Account, Source}) ->
    CallbackArgs =
    case get_comeback_from_cache(Id, Account, Source) of
        {?DATA_STATE_UNUSE = DataState, UnusedList} ->
            % ?INFO("DATA_STATE_UNUSE: [~w,~ts]", [Id, Account]),
            %% 数据未被使用过
            {DataIdList, ComebackHis} = merge_data_list(UnusedList),
            case ComebackHis of
                #comeback_history{} ->
                    case CheckType of
                        check_get ->
                            %% 以数据库更新结果来锁定最终执行结果
                            case set_comeback_data_used(DataIdList, Id) of
                                true  ->
                                    set_comeback_data_used_cache(Account, DataIdList, Id),
                                    [Id, DataState, ?COMEBACK_STATE_GOT, ComebackHis];
                                false ->
                                    % 数据未更新到，可能是被并发的其他请求修改掉了，忽略，等下次再尝试处理
                                    ignore
                            end;
                        _ ->
                            [Id, DataState, ?COMEBACK_STATE_NOTIFIED, ComebackHis]
                    end;
                _ ->    % 忽略，下次触发时会再尝试处理
                    ignore
            end;
        {?DATA_STATE_USED = DataState, []} ->
            % ?INFO("DATA_STATE_USED: [~w,~ts] , by other", [Id, Account]),
            %% 已被其他角色捷足先登
            [Id, DataState, ?COMEBACK_STATE_OTHER_GOT, undefined];
        {?DATA_STATE_USED = DataState, MatchUsedList} ->
            % ?INFO("DATA_STATE_USED: [~w,~ts] , by myself", [Id, Account]),
            %% 已由自己获得
            {_DataIdList, ComebackHis} = merge_data_list(MatchUsedList),
            case ComebackHis of
                #comeback_history{} ->
                    %% 避免游戏服未保存成功，只要有请求，就重新从已触发的返利数据中，获取账号相同，
                    %% 并且是标示为已发送给自己的项目中取得奖励数据，通知游戏服发放奖励
                    %% 游戏服通过更新指定状态的数据，来避免重复发送奖励，保证只能发一次
                    % ?INFO_TTY("retry return: id ~w, dataIds ~w", [Id, _DataIdList]),
                    [Id, DataState, ?COMEBACK_STATE_GOT, ComebackHis];
                false ->
                    ignore
            end;
        undefined = DataState ->
            % ?INFO("DATA NOT DEFINED: [~w,~ts]", [Id, Account]),
            %% 没有历史数据的角色
            [Id, DataState, ?COMEBACK_STATE_NOT_BACK, undefined];
        _ ->
            %% 查找数据出错（查数据库时才有可能失败，ETS缓存不会执行到这里）
            ?ERR("find comeback data error, serverId[~w], id[~w], account[~ts], source[~ts]",
                [ServerId, Id, Account, Source]
            ),
            error
    end,
    case CallbackArgs of
        [_Id, _State, _ComebackState, _ComebackHis] ->
            clusters_center:apply_cast(
                ServerId,
                {?MODULE, check_return_callback, CallbackArgs}
            );
        _ -> ignore
    end.

%% 跨服模式游戏服回调处理回归状态与奖励
%% DataState    : ?DATA_STATE_USED | ?DATA_STATE_UNUSE | undefined
%% ComebackState: ?COMEBACK_STATE_GOT | ...
%% ComebackHis  : #comeback_history{} | undefined
check_return_callback(Id, _DataState, ComebackState, ComebackHis) ->
    % ?INFO("callback : [~w], OldState:~w, NewState:~w", [Id, _DataState, ComebackState]),
    Pid = misc:get_player_process(Id),
    IsOnline = is_pid(Pid) andalso is_process_alive(Pid),
    if
        IsOnline andalso ComebackState =:= ?COMEBACK_STATE_GOT ->
            %% 数据状态不论是否使用过，只要标记为自己应获得的，通知玩家检测一下是不是已经发放奖励
            lib_player:apply_cast(Pid, ?APPLY_CAST_STATUS, ?MODULE, do_give_awards, [ComebackHis]);
        IsOnline ->
            %% 无历史数据、数据已经由他人使用、需要通知前端封测信息等，在线时由角色进程设置状态或进行通知
            lib_player:apply_cast(Pid, ?APPLY_CAST_STATUS, ?MODULE, do_set_comeback_state, [ComebackState, ComebackHis]);
        ComebackState =:= ?COMEBACK_STATE_GOT ->
            %% 自己应获得的，不在线则忽略消息（下一次在线时会重新尝试）
            ignore;
        ComebackState =:= ?COMEBACK_STATE_NOTIFIED ->
            %% 不在线忽略需要通知前端封测信息的消息
            ignore;
        true ->
            %% 其他情况（无历史数据、数据已经由他人使用等），不在线时直接更新状态
            update_comeback_state(Id, ComebackState)
    end.

%% 从缓存ETS表中查询角色回归历史数据
%% -> {?DATA_STATE_UNUSE, UnusedList}
%%  | {?DATA_STATE_USED, MatchUsedList}
%%  | undefined
%%  MatchUsedList为[] 表明对其他同账号角色返利过了，当前角色不需要返利
get_comeback_from_cache(Id, Account, _Source) ->
    case ets:lookup(?COMEBACK_TAB, change_case(lower, Account)) of
        [#comeback_cache{list = List}] ->
            find_match_data(Id, List);
        _ ->    % 没有数据
            undefined
    end.

%% 从数据库中查询角色回归历史数据
%% -> {?DATA_STATE_UNUSE, UnusedList}
%%  | {?DATA_STATE_USED, MatchUsedList}
%%  | undefined
%%  | false
%%  MatchUsedList为[] 表明对其他同账号角色返利过了，当前角色不需要返利
get_comeback_from_db(Id, Account, _Source) ->
    % %% 按source倒序排序，确保存在多个source时，source=''在最后，优先取对应的Source
    % Sql = util:fmt_bin("SELECT `id`,`state`,`source`,`data`,`now_role_id` FROM comeback WHERE account='~ts' AND (source='~ts' OR source='') ORDER BY source DESC", [Account, Source]),
    %% 最新规则，只要账号一致就返，不管渠道是否一致
    %% 如此一来需要合并多条数据进行返利（而非一条），逻辑需调整
    Sql = util:fmt_bin("SELECT `id`,`state`,`source`,`data`,`now_role_id` FROM comeback WHERE account='~ts'", [Account]),
    try
        case db:get_all(Sql) of
            [_ | _] = List ->
                NewList = [conv_comeback_data(Data) || Data <- List],
                find_match_data(Id, NewList);
            [] ->       % 没有数据
                undefined;
            _ ->
                false
        end
        catch _:_ ->
            false
    end.

find_match_data(Id, DataList) ->
    {MatchUsedList, _UnmatchUsedList, UnusedList} = partition_used(Id, DataList, {[], [], []}),
    case {MatchUsedList, UnusedList} of
        {[_ | _], _} ->
            %% 已经存在本角色使用的数据
            % ?INFO_TTY("Me used:~w", [{MatchUsedList, UnusedList}]),
            {?DATA_STATE_USED, MatchUsedList};
        {_, [_ | _]} ->
            %% 不存在本角色数据，存在未使用数据
            % ?INFO_TTY("Data unused:~w", [{MatchUsedList, UnusedList}]),
            {?DATA_STATE_UNUSE, UnusedList};
        {_, _} ->
            %% 由其他角色使用掉了
            % ?INFO_TTY("others used:~w", [{MatchUsedList, UnusedList}]),
            {?DATA_STATE_USED, []}
    end.

%% -> {MatchUsedList, UnmatchUsedList, UnusedList}
%% MatchUsedList = UnmatchUsedList = UnusedList : [{DataId, BinData}]
partition_used(Id, [
        #comeback_data{
            data_id = DataId, state = ?DATA_STATE_USED,
            data = Data, now_role_id = NowRoleId
        } | RemainList
    ],
    {MatchUsedList, UnmatchUsedList, UnusedList}) ->
    case NowRoleId =:= Id of
        true  ->
            NewMatchUsedList = [{DataId, Data} | MatchUsedList],
            partition_used(Id, RemainList, {NewMatchUsedList, UnmatchUsedList, UnusedList});
        false ->
            NewUnmatchUsedList = [{DataId, Data} | UnmatchUsedList],
            partition_used(Id, RemainList, {MatchUsedList, NewUnmatchUsedList, UnusedList})
    end;
partition_used(Id, [
        #comeback_data{
            data_id = DataId, state = ?DATA_STATE_UNUSE,
            data = Data
        } | RemainList
    ],
    {MatchUsedList, UnmatchUsedList, UnusedList}) ->
    NewUnusedList = [{DataId, Data} | UnusedList],
    partition_used(Id, RemainList, {MatchUsedList, UnmatchUsedList, NewUnusedList});
partition_used(Id, [_ | List], AccList) ->
    partition_used(Id, List, AccList);
partition_used(_, [], AccInfo) ->
    AccInfo.

%% 游戏服模式发放回归奖励
do_give_awards(Player = #player_status{id = Id,
        figure = #figure{name = _Name}, comeback_state = OldState
    }, ComebackHis = #comeback_history{charge_gold = _ChargeGold}) ->
    case lists:member(OldState, [?COMEBACK_STATE_UNDO, ?COMEBACK_STATE_NOTIFIED]) of
        true  ->
            % ?INFO("cross return: [~w,~ts]", [Id, _Name]),
            NewComebackState = ?COMEBACK_STATE_GOT,
            case update_comeback_state(Id, NewComebackState, OldState) of
                true  ->        % 需要更新DB时验证旧状态，可以避免并发因素导致的重复发放
                    case calc_awards(ComebackHis) of
                        []      ->
                            % ?INFO("[~w,~ts] awards is empty!", [Id, _Name]),
                            ignore;
                        Awards  ->
                            % ?INFO("send awawd mail to role:[~w,~ts], OldState:~w, his:~w", [Id, _Name, OldState, ComebackHis]),
                            Title   = <<"回归充值返利"/utf8>>,
                            Content = <<"尊敬的大人\n"
                            "每次相遇都是久别重逢，欢迎您再次参与本次游戏不删档测试，作为已经参与过游戏删档付费测试的老朋友，与您相约的充值返利在此为您奉上啦！\n"
                            "在久别日子里我们对游戏进行优化改动，给与你不一样的游戏世界。您的光荣是我们的荣幸，您的意见（建议）是我们的方向。\n"
                            "如果您在游戏中遇到问题，可通过游戏中反馈功能或关注微信公众号：mlgame进行建议反馈，感谢您对游戏的支持。\n"
                            "以下是根据您在删档付费测试期间充值金额对应获得的充值返利，请查收！"/utf8>>,
                            lib_mail:send_sys_mail([Id], ?MOD_NONE, ?MOD_NONE, Title, Content, Awards)
                    end,
                    {ok, Player#player_status{comeback_state = ?COMEBACK_STATE_GOT}};
                false ->
                    % ?INFO("do_give_awards db state unmatch[~w,~ts], OldState:~w", [Id, _Name, OldState]),
                    error
            end;
        false ->
            % ?INFO("role state unmatch[~w,~ts], OldState:~w", [Id, _Name, OldState]),
            ignore
    end;
do_give_awards(_, _) ->
    ignore.

calc_awards(#comeback_history{charge_gold = ChargeGold, login_days = LoginDays}) when ChargeGold > 0 ->
    Ratio =
    if
        LoginDays >= 7 -> 1;
        LoginDays >= 5 -> 0.85;
        LoginDays >= 4 -> 0.75;
        LoginDays >= 2 -> 0.5;
        LoginDays >= 1 -> 0.2;
        true -> 0
    end,
    Base = min(ChargeGold, 50000),
    BindGold = umath:floor(Base * Ratio),
    [{?TYPE_GOLD, 0, ChargeGold}, {?TYPE_BGOLD, 0, BindGold}];
calc_awards(_) ->
    [].

%% 玩家进程修改comeback_state
do_set_comeback_state(Player = #player_status{id = Id, sid = Sid},
    NewComebackState, ComebackHis) ->
    case NewComebackState =:= ?COMEBACK_STATE_NOTIFIED of
        true  ->    %% 需要通知玩家封测的状态
            case ComebackHis of
                #comeback_history{charge_gold = ChargeGold} ->
                    lib_server_send:send_to_sid(Sid, pt_417, 41799, [ChargeGold, ?RETURN_LEVEL]);
                _ ->
                    ignore
            end;
        false -> ignore
    end,
    update_comeback_state(Id, NewComebackState),
    NewPlayer = Player#player_status{comeback_state = NewComebackState},
    {ok, NewPlayer}.

set_comeback_data_used_cache(Account, DataIdList = [_|_], NowRoleId) ->
    LowerAccount = change_case(lower, Account),
    case ets:lookup(?COMEBACK_TAB, LowerAccount) of
        [#comeback_cache{list = OldList} = OldCache] ->
            NewList = set_cache_data_used(OldList, DataIdList, NowRoleId),
            NewCache = OldCache#comeback_cache{list = NewList},
            ets:insert(?COMEBACK_TAB, NewCache);
        _ ->
            ignore
    end;
set_comeback_data_used_cache(_Account, _, _) ->
    ignore.

set_cache_data_used(DataList, [Id | Others], NowRoleId) ->
    NewDataList =
    case lists:keytake(Id, #comeback_data.data_id, DataList) of
        {value, MatchData = #comeback_data{}, RemainList} ->
            NewData = MatchData#comeback_data{now_role_id = NowRoleId, state = ?DATA_STATE_USED},
            [NewData | RemainList];
        _ ->
            DataList
    end,
    set_cache_data_used(NewDataList, Others, NowRoleId);
set_cache_data_used(DataList, [], _) ->
    DataList.

%% 设置状态为已使用（true表明有更新到数据）
set_comeback_data_used(DataIdList = [_ | _], NowRoleId) ->
    Sql =
    case DataIdList of
        [DataId] ->
            util:fmt_bin("UPDATE comeback SET `state`=~w,now_role_id='~w' WHERE `id`=~w AND `state`=~w",
                [?DATA_STATE_USED, NowRoleId, DataId, ?DATA_STATE_UNUSE]
            );
        _ ->
            DataIdsStr = util:join_bitstring([integer_to_binary(X) || X <- DataIdList] , <<",">>),
            util:fmt_bin("UPDATE comeback SET `state`=~w,now_role_id='~w' WHERE `id` IN (~ts) AND `state`=~w",
                [?DATA_STATE_USED, NowRoleId, DataIdsStr, ?DATA_STATE_UNUSE]
            )
    end,
    db:execute(Sql) > 0.

update_comeback_state(Id, SetState) ->
    Sql = util:fmt_bin("UPDATE player_login SET comeback_state=~w WHERE id=~w", [SetState, Id]),
    db:execute(Sql).

update_comeback_state(Id, SetState, OldState) ->
    Sql = util:fmt_bin("UPDATE player_login SET comeback_state=~w WHERE id=~w AND comeback_state=~w", [SetState, Id, OldState]),
    db:execute(Sql) > 0.

merge_data_list(DataList) ->
    lists:foldl(fun({DataId, Data}, {DataIdList, ComebackHis}) ->
                case gen_comeback_history(Data) of
                    DataHistory = #comeback_history{} ->
                        NewDataIdList = [DataId | DataIdList],
                        {NewDataIdList, merge_data(ComebackHis, DataHistory)};
                    _ ->
                        {DataIdList, ComebackHis}
                end;
            (_, Acc) ->
                Acc
        end, {[], #comeback_history{}}, DataList).

merge_data(#comeback_history{
        role_level = LevelA
        ,charge_gold = ChargeGoldA
        ,login_days = LoginDaysA
    }, #comeback_history{
        role_level = LevelB
        ,charge_gold = ChargeGoldB
        ,login_days = LoginDaysB
    }) ->
    #comeback_history{
        role_level = max(LevelA, LevelB)
        ,charge_gold = ChargeGoldA + ChargeGoldB
        ,login_days = max(LoginDaysA, LoginDaysB)
    }.

%% 根据数据库中的字符串数据生成#comeback_history{}
gen_comeback_history(BinData) when is_binary(BinData) ->
    case util:bitstring_to_term(BinData) of
        DataList when is_list(DataList) ->
            setelement_list(#comeback_history{}, record_info(fields, comeback_history), DataList);
        _Undefined ->       % 数据不正确
            false
    end;
gen_comeback_history(Data = #comeback_history{}) ->
    Data;
gen_comeback_history(_) ->
    false.

%% 根据标签设置Record的相应字段值
setelement_list(Record, Fields, [{Label, Val} | Others]) ->
    case ulists:index_of(Label, Fields) of
        0 ->        % 找不到这个标签，无用数据，忽略
            setelement_list(Record, Fields, Others);
        Index ->
            NewTuple = erlang:setelement(Index + 1, Record, Val),
            setelement_list(NewTuple, Fields, Others)
    end;
setelement_list(Record, _Fields, []) ->
    Record.

%% 初始化加载数据到ETS表缓存
-spec init_load_data() -> ok.
init_load_data() ->
    ets:new(?COMEBACK_TAB, [{keypos, #comeback_cache.account}, named_table, public, set, {read_concurrency, true}]),
    % T1 = utime:unixtime(ms),
    load_data(),
    % T2 = utime:unixtime(ms),
    % ?INFO_TTY("use time:~w ms", [T2 - T1]),
    ok.

reload_data() ->
    ets:delete_all_objects(?COMEBACK_TAB),
    load_data().

load_data() ->
    AllDataList = db:get_all("SELECT `account`,`id`,`state`,`source`,`data`,`now_role_id` FROM `comeback`"),
    classify_insert_by_account(AllDataList).

classify_insert_by_account([[Account, Id, State, Source, BinData, NowRoleId] | Others]) ->
    LowerAccount = change_case(lower, Account),
    Data    = gen_comeback_history(BinData),
    AddData = conv_comeback_data([Id, State, Source, Data, NowRoleId]),
    NewCache =
    case ets:lookup(?COMEBACK_TAB, LowerAccount) of
        [#comeback_cache{list = OldList} = OldCache] ->
            OldCache#comeback_cache{list = [AddData | OldList]};
        [] ->
            #comeback_cache{account = LowerAccount, list = [AddData]}
    end,
    ets:insert(?COMEBACK_TAB, NewCache),
    classify_insert_by_account(Others);
classify_insert_by_account([_ | Others]) ->
    classify_insert_by_account(Others);
classify_insert_by_account([]) ->
    ok.

conv_comeback_data([Id, State, Source, Data, NowRoleId]) ->
    OneComebackHis = gen_comeback_history(Data),
    #comeback_data{
        data_id = Id, state = State, source = Source, data = OneComebackHis, now_role_id = NowRoleId
    }.

%% GM重设状态为未处理过 -> NewPlayer.
gm_set_role_state(Player = #player_status{id = Id, comeback_state = OldState}) ->
    %% 测试只有设置此状态有意义，其他状态需要配合其他数据修改，GM命令暂不支持
    SetState = ?COMEBACK_STATE_UNDO,
    case OldState =/= SetState of
        true  ->
            update_comeback_state(Id, SetState),
            Player#player_status{comeback_state = SetState};
        false ->
            Player
    end.

%% ----------------------------------------------------------------
%% 从历史服导出数据，用于新服回归活动
%% NOTE: 废弃不再更新维护和使用，后面由运营直接提供封测玩家有价值的数据
%%   处理好之后，直接写入到数据库，无须使用export_sql/0来导出
%% ----------------------------------------------------------------

export_sql() ->
    MysqlPoolPid = whereis(mysql_dispatcher),
    case is_pid(MysqlPoolPid) of
        true  -> ignore;
        false ->    % 数据库连接未启动，可能是游戏服未开启的情况下取数据
            %% 启动时，必须指定config和path：erl -config gs.config -pa ../ebin
            db:connect_mysql()
    end,
    {{Y, M, D}, _Time} = erlang:localtime(),
    ExportFile = iolist_to_binary(io_lib:format("../logs/comeback_~w~2..0w~2..0w.sql", [Y, M, D])),
    case file:open(ExportFile, [write, {encoding, utf8}]) of
        {ok, File} ->
            erlang:put(sql_file, File),
            try
                % accname为mxxxchatXXXX，source为zxy的账号是聊天监控软件登录创建，为避免数据合服冲突，直接过滤这种非玩家号
                RoleList = db:get_all(<<"SELECT accname,source,id FROM player_login WHERE accname NOT LIKE 'mxxxchat%' ORDER BY accname,source"/utf8>>),
                CreateTableSql = get_create_table_sql(),
                io:format(File, "SET NAMES UTF8;~n~ts~n~ts~n", [CreateTableSql, "LOCK TABLES `comeback` WRITE;"]),
                % 查出所有角色，按account,source排序；然后对所有这些角色做处理，每次挑一个account的N个角色数据出来进行处理
                handle_role_list(RoleList, []),
                io:format(File, "~ts~n", ["UNLOCK TABLES;"]),
                ?INFO("succeed to export account charge info [file: ~ts]", [ExportFile])
                catch _ExType:_ExPattern ->
                    ?ERR("failed to get account list:~w", [{_ExType, _ExPattern, erlang:get_stacktrace()}])
                after
                    file:close(File)
            end;
        {error, _Err} ->
            ?ERR("failed to open file:~w", [_Err])
    end.

%% 处理所有的账号数据
%% 传入的参数中，已经保证了同一账号的角色放在相邻位置，这个方法依赖于这个关系，才能取得正确信息
handle_role_list([RoleInfo = [_Account, _Source, _RoleId] | RemainList], []) ->
    %% 当前处理列表为空，加入列表中
    handle_role_list(RemainList, [RoleInfo]);

handle_role_list([RoleInfo = [Account, Source, _RoleId] | RemainList], AccountRoles = [[Account, Source, _] | _]) ->
    %% 与当前处理列表中的角色数据属于同一个账号，并且同一个渠道包，加入列表中
    handle_role_list(RemainList, [RoleInfo | AccountRoles]);

handle_role_list([RoleInfo = [AccountB, SourceB, RoleIdB] | RemainList], AccountRoles = [[AccountA, SourceA, RoleIdA] | RestAccountRoles]) ->
    %% 特殊情况，如果AccountB与AccountA只是大小写的差异（甚至两边有空格的差异:-(）应计为一个账号
    LowerAccountB = strip(change_case(lower, AccountB)),
    LowerAccountA = strip(change_case(lower, AccountA)),
    LowerSourceB  = strip(change_case(lower, SourceB)),
    LowerSourceA  = strip(change_case(lower, SourceA)),
    case LowerAccountA =:= LowerAccountB andalso LowerSourceA =:= LowerSourceB of
        true  ->    % 还是同一个账号，只是大小写不一样（全转换为小写）
            NewAccountRoles = [
                [LowerAccountB, LowerSourceB, RoleIdB], [LowerAccountA, LowerSourceA, RoleIdA]
                | [[strip(change_case(lower, Account)), strip(change_case(lower, Source)), RoleId] || [Account, Source, RoleId] <- RestAccountRoles]
            ],
            handle_role_list(RemainList, NewAccountRoles);
        false ->    % 不是同一个账号，需要开始处理这个人的数据
            handle_account_info(AccountRoles),
            handle_role_list(RemainList, [RoleInfo])
    end;

handle_role_list([], AccountRoles = [_ | _]) ->
    handle_account_info(AccountRoles),
    handle_role_list([], []);

handle_role_list([], []) ->
    ok.

handle_account_info(RoleList = [[Account, Source, _] | _]) ->
    SqlGetChargeGold = util:fmt_bin("SELECT COALESCE(SUM(c.gold), 0) FROM charge c LEFT JOIN player_login p ON c.player_id=p.id WHERE p.accname = '~ts' AND p.source = '~ts' AND c.gold > 0", [Account, Source]),
    ChargeGold       = db:get_one(SqlGetChargeGold),
    if
        ChargeGold < ?EXPORT_CHARGE ->
            ignore;
        true ->
            RoleDiffInfos   = get_account_role_diff_infos(RoleList, []),
            MaxRoleLevel    = lists:max([RoleLevel || {_RoleId, _RoleName, RoleLevel, _ChargeTotal, _LoginDays} <- RoleDiffInfos]),
            MaxLoginDays    = lists:max([LoginDays || {_RoleId, _RoleName, _RoleLevel, _ChargeTotal, LoginDays} <- RoleDiffInfos]),
            %% 记录旧角色数据，便于查询 id:name:level:charge, ...
            RoleInfos = util:join_bitstring([
                    util:fmt_bin("~w:~ts:~w:~w:~w", [RoleId, RoleName, RoleLevel, ChargeTotal, LoginDays])
                    || {RoleId, RoleName, RoleLevel, ChargeTotal, LoginDays} <- RoleDiffInfos
                ], <<",">>),
            Data = [{role_level, MaxRoleLevel}, {charge_gold, ChargeGold}, {login_days, MaxLoginDays}],
            SqlExport = util:fmt_bin(
                "INSERT INTO `comeback` (`account`,`source`,`state`,`data`,`old_role`,`now_role_id`) VALUES ('~ts','~ts','~w','~ts','~ts',0);",
                [Account, Source, 0, util:term_to_bitstring(Data), RoleInfos]
            ),
            File = erlang:get(sql_file),
            io:format(File, "~ts~n", [SqlExport]),
            ok
    end.

%% 获取角色累计登录天数
%% select player_id,COUNT(*) FROM (SELECT DISTINCT t,player_id FROM (SELECT DISTINCT DATE_FORMAT(FROM_UNIXTIME(`time`), "%Y%m%d") as t,player_id FROM log_login UNION SELECT DISTINCT DATE_FORMAT(FROM_UNIXTIME(`time`), "%Y%m%d") as t,player_id FROM log_offline) AS login_data) AS ld GROUP BY player_id;

get_account_role_diff_infos([[Account, _Source, RoleId] | RemainList], RoleDiffInfos) ->
    Sql = util:fmt_bin("SELECT pl.nickname,COALESCE(pl.lv, 0),COALESCE(SUM(c.gold), 0) FROM player_low pl LEFT JOIN charge c ON pl.id=c.player_id WHERE pl.id=~w", [RoleId]),
    case db:get_row(Sql) of
        [RoleName, RoleLevel, ChargeTotal] ->
            SqlGetLogindays = util:fmt_bin("SELECT COUNT(DISTINCT t) FROM (SELECT DISTINCT DATE_FORMAT(FROM_UNIXTIME(`time`), '%Y%m%d') as t FROM log_login WHERE player_id = ~w UNION SELECT DISTINCT DATE_FORMAT(FROM_UNIXTIME(`time`), '%Y%m%d') as t FROM log_offline WHERE player_id = ~w) AS login_data", [RoleId, RoleId]),
            Logindays =
            case db:get_one(SqlGetLogindays) of
                Count when is_integer(Count) -> Count;
                _ -> 1
            end,
            NewRoleDiffInfos = [{RoleId, RoleName, RoleLevel, ChargeTotal, Logindays} | RoleDiffInfos],
            get_account_role_diff_infos(RemainList, NewRoleDiffInfos);
        [] ->
            ErrMsg = util:fmt_bin("fetch role error : [account:~ts, ~w]", [Account, RoleId]),
            ?ERR("~ts", [ErrMsg]),
            throw(ErrMsg)
    end;
get_account_role_diff_infos([], RoleDiffInfos) ->
    RoleDiffInfos.

get_create_table_sql() ->
    <<"CREATE TABLE IF NOT EXISTS `comeback` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `account` varchar(64) NOT NULL COMMENT '账号',
    `source` varchar(50) NOT NULL DEFAULT '' COMMENT '渠道来源',
    `state` tinyint(3) NOT NULL COMMENT '状态(0未返/1已返)',
    `data` text NOT NULL COMMENT '历史数据',
    `old_role` text COMMENT '旧号信息',
    `now_role_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '已返利的角色id',
    PRIMARY KEY (`id`),
    KEY `account` (`account`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='封测玩家回归数据';
"/utf8>>.

%% 大小写转换(多字节安全)，返回binary
change_case(CaseType, CharactersData) ->
    ChangeFun =
    case CaseType of
        lower ->    % 转小写
            fun string:to_lower/1;
        _Upper ->   % 转大写
            fun string:to_upper/1
    end,
    unicode:characters_to_binary(ChangeFun(unicode:characters_to_list(CharactersData))).

strip(CharactersData) ->
    unicode:characters_to_binary(string:strip(unicode:characters_to_list(CharactersData))).


