%%-----------------------------------------------------------------------------
%%% @author liushl
%%% @doc
%%%     玩家消费数据/每天定时清理超过30天的消费记录
%%%     玩家的消费记录只有30天的有效期！！！
%%% @end
%%-----------------------------------------------------------------------------

-module (lib_consume_data).

-export ([
        update_money_consume/3                  %% 消费完更新消费记录
        , get_gold_consume_between/3            %% 元宝消费数据
        , get_bgold_consume_between/3           %% 绑元消费数据-包括元宝
        , get_bgold_only_consume_between/3      %% 绑元消费数据-只计算绑元
        , get_coin_consume_between/3            %% 铜币消费数据
        , get_consume_between/4

        , logout_check/1
        , db_clean_consume_date/1               %% 每天定时清理超过30天的消费记录
        , get_consume_gold_roles/2              %% 从数据库中，获取指定时间段有gold类消费的角色列表
        , get_consume_gold_role_ids/2

        %% 预付款处理
        , handle_advance_payment/8
        , advance_cost_objects/5
        , advance_payment_done/2
        , advance_payment_fail/3
        , save_advance_payments/1
    ]).

-include ("common.hrl").
-include ("predefine.hrl").
-include ("server.hrl").
-include ("errcode.hrl").
-include ("figure.hrl").
-include("def_event.hrl").
-include ("consume.hrl").
-include("rec_event.hrl").
-include("vip.hrl").
-include("def_goods.hrl").

%%%-------------------------------------------------------------------
%%% @doc
%%%      获得某个时间段内的消费数据。
%%%      注意！！！消费数据记录的时间是每天的0点，所以取数据时要注意时间的选取
%%% @end
%%%-------------------------------------------------------------------
%% 元宝消费数据
get_gold_consume_between(RoleId, StartTime, EndTime) ->
    get_consume_between(RoleId, gold, make_sure_zero_clock(StartTime), EndTime).

%% 绑元消费数据-包括元宝
get_bgold_consume_between(RoleId, StartTime, EndTime) ->
    get_consume_between(RoleId, gold_bgold, make_sure_zero_clock(StartTime), EndTime).

%% 绑元消费数据-只计算绑元
get_bgold_only_consume_between(RoleId, StartTime, EndTime) ->
    get_consume_between(RoleId, bgold, make_sure_zero_clock(StartTime), EndTime).

%% 铜币消费数据
get_coin_consume_between(RoleId, StartTime, EndTime) ->
    get_consume_between(RoleId, coin, make_sure_zero_clock(StartTime), EndTime).

get_consume_between(RoleId, Type, StartTime, EndTime) ->
    RoleConsume = get_consume_data(RoleId),
    get_consume_since_by_type(Type, RoleConsume, make_sure_zero_clock(StartTime), EndTime).

get_consume_since_by_type(gold, #role_consume{consume_data = ConsumeData}, StartTime, EndTime) ->
    lists:sum([Value || #consume_data{gold = Value, date = Date} <- ConsumeData, Date >= StartTime, Date =< EndTime]);
get_consume_since_by_type(gold_bgold, #role_consume{consume_data = ConsumeData}, StartTime, EndTime) ->
    lists:sum([Gold +  BGold|| #consume_data{gold = Gold, bgold = BGold, date = Date} <- ConsumeData, Date >= StartTime, Date =< EndTime]);
get_consume_since_by_type(bgold, #role_consume{consume_data = ConsumeData}, StartTime, EndTime) ->
    lists:sum([Value || #consume_data{bgold = Value, date = Date} <- ConsumeData, Date >= StartTime, Date =< EndTime]);
get_consume_since_by_type(coin, #role_consume{consume_data = ConsumeData}, StartTime, EndTime) ->
    lists:sum([Value || #consume_data{coin = Value, date = Date} <- ConsumeData, Date >= StartTime, Date =< EndTime]).

make_sure_zero_clock(Time) ->
    utime:unixdate(Time).
%%%-------------------------------------------------------------------
%%% @doc
%%%      消费完更新消费记录
%%% @end
%%%-------------------------------------------------------------------
update_money_consume(NewPlayer, Player, {advance_payment, Ref}) ->
    update_advance_payment(NewPlayer, Player, Ref);
update_money_consume(NewPlayer, Player, ConsumeType) ->
    case data_produce_type:is_consume_for_act(ConsumeType) of
        1 ->
            Gold  = Player#player_status.gold - NewPlayer#player_status.gold,
            BGold = Player#player_status.bgold - NewPlayer#player_status.bgold,
            Coin  = Player#player_status.coin - NewPlayer#player_status.coin,
            update_money_consume(NewPlayer#player_status.id, Gold, BGold, Coin);
        _ ->
            ignore
    end.

update_money_consume(_RoleId, 0, 0, 0) -> ok;
update_money_consume(RoleId, Gold, BGold, Coin) ->
    #role_consume{consume_data = ConsumeData} =
        RoleConsume = get_consume_data(RoleId),
    ToDay = utime:unixdate(),
    case lists:keyfind(ToDay, #consume_data.date, ConsumeData) of
        #consume_data{} = Data ->
            NewData = Data#consume_data{
                gold = Data#consume_data.gold + Gold,
                bgold = Data#consume_data.bgold + BGold,
                coin = Data#consume_data.coin + Coin,
                update = true
            },
            NewConsumeData = [NewData|lists:keydelete(ToDay, #consume_data.date, ConsumeData)];
        _ ->
            NewData = #consume_data{
                date = ToDay,
                gold = Gold,
                bgold = BGold,
                coin = Coin,
                update = true
            },
            NewConsumeData = [NewData|ConsumeData]
    end,
    NewRoleConsume = RoleConsume#role_consume{consume_data = NewConsumeData},
    check_save_consume(RoleId, NewRoleConsume, false).

logout_check(RoleId) ->
    RoleConsume = get_consume_data(RoleId),
    check_save_consume(RoleId, RoleConsume, true).

%% 检查保存消费记录
check_save_consume(RoleId, RoleConsume, Force) ->
    #role_consume{
        consume_data = ConsumeData,
        save_time = SaveTime
    } = RoleConsume,
    Now = utime:unixtime(),
    case Force orelse (Now - SaveTime > 60) of
        true  ->
            %% 每隔一段时间保存一次，遍历是为了防止跨天的情况下漏保存前一天的数据
            %% 大部分情况下是不会多写数据库，只是会多遍历一次，但是数据不是很多, 最多判断前3条数据，即可保证数据跨天的数据能保存到
            %% 下线的时候会强制判断一次
            NewConsumeData = check_save_consume_do(RoleId, ConsumeData, 1),
            put_consume_data(RoleId, RoleConsume#role_consume{
                consume_data = NewConsumeData,
                save_time = Now
            });
        false ->
            put_consume_data(RoleId, RoleConsume)
    end.

check_save_consume_do(_, [], _) ->
    [];
check_save_consume_do(_, List, 4) ->
    List;
check_save_consume_do(RoleId, [One|T] = List, N) ->
    case One#consume_data.update of
        true  ->
            NewOne = save_consume_data(RoleId, One),
            [NewOne|check_save_consume_do(RoleId, T, N+1)];
        false ->
            %% 已经保存了，认为后面的也保存了，不用继续判断
            List
    end.

%%%-------------------------------------------------------------------
%%% @doc
%%%      获得消费数据
%%% @end
%%%-------------------------------------------------------------------
get_consume_data(RoleId) ->
    case get({?MODULE, RoleId}) of
        undefined ->
            RoleConsume = load_consume_date(RoleId),
            put({?MODULE, RoleId}, RoleConsume),
            RoleConsume;
        #role_consume{} = RoleConsume ->
            RoleConsume
    end.

put_consume_data(RoleId, #role_consume{} = RoleConsume) ->
    put({?MODULE, RoleId}, RoleConsume);
put_consume_data(RoleId, Data) ->
    ?ERR("consume data error:~p,~p", [RoleId, Data]),
    ok.

%% 玩家全额加载所有消费数据，最多30条
load_consume_date(RoleId) ->
    SQL = io_lib:format(<<"SELECT zero_time,gold,bgold,coin FROM daily_consume_log WHERE role_id=~p">>, [RoleId]),
    ConsumeData =
        [
            #consume_data{
                date    = ZeroTime
                , gold  = Gold
                , bgold = Bgold
                , coin  = Coin
            } || [ZeroTime, Gold, Bgold, Coin] <- db:get_all(SQL)
        ],
    #role_consume{consume_data = ConsumeData}.

%% 保存尚未保存的数据
save_consume_data(_RoleId, #consume_data{update = false} = Data) ->Data;
save_consume_data(RoleId, Data) ->
    #consume_data{date    = Date
        , gold  = Gold
        , bgold = BGold
        , coin  = Coin} = Data,
    SQL = io_lib:format(<<"REPLACE INTO daily_consume_log(role_id,zero_time,gold,bgold,coin) VALUES(~p,~p,~p,~p,~p)">>, [RoleId, Date, Gold, BGold, Coin]),
    db:execute(SQL),
    Data#consume_data{update = false}.

%% 从数据库中，获取指定时间段有gold类消费的角色列表，包括角色id及其gold类货币消费数额
%% 数据库内的时间参数是每天的0点，所以需要注意结束时间的选取
%% 注意时间范围区间，为左闭右开
get_consume_gold_roles(StartTime, EndTime) ->
    Sql = io_lib:format("SELECT role_id,SUM(gold),SUM(bgold),SUM(coin) FROM daily_consume_log WHERE `zero_time`>=~w AND `zero_time`<~w GROUP BY role_id", [StartTime, EndTime]),
    db:get_all(Sql).

%% 从数据库中，获取指定时间段有gold类消费的角色id列表
%% 数据库内的时间参数是每天的0点，所以需要注意结束时间的选取
%% 注意时间范围区间，为左闭右开
get_consume_gold_role_ids(StartTime, EndTime) ->
    Sql = io_lib:format("SELECT DISTINCT role_id FROM daily_consume_log WHERE `zero_time`>=~w AND `zero_time`<~w", [StartTime, EndTime]),
    db:get_all(Sql).

db_clean_consume_date(7) ->
    spawn(
        fun() ->
            timer:sleep(5000),
            Time = utime:unixtime() - 30*86400,
            SQL = io_lib:format(<<"DELETE FROM daily_consume_log WHERE zero_time<~p">>, [Time]),
            db:execute(SQL)
        end
    );
db_clean_consume_date(_) -> ok.
%%%-------------------------------------------------------------------
%%% @doc
%%%      TODO 预付款处理，继承过来的代码，未验证
%%% @end
%%%-------------------------------------------------------------------
update_advance_payment(NewPlayer, Player, Ref) ->
    Coin = {Player#player_status.coin, NewPlayer#player_status.coin},
    Gold = {Player#player_status.gold, NewPlayer#player_status.gold},
    BGold = {Player#player_status.bgold, NewPlayer#player_status.bgold},
    FGold = {Player#player_status.fgold, NewPlayer#player_status.fgold},
    case self() =:= Player#player_status.pid of
        true ->
            handle_advance_payment(Player#player_status.id, Coin, Gold, BGold, FGold, Ref, Player#player_status.figure#figure.lv, Player#player_status.vip_status#vip_status.lv);
        _ ->
            lib_player:apply_cast(Player#player_status.pid, ?APPLY_CAST, ?MODULE, handle_advance_payment, [Player#player_status.id, Coin, Gold, BGold, FGold, Ref, Player#player_status.figure#figure.lv, Player#player_status.vip_status#vip_status.lv])
    end.

handle_advance_payment(RoleId, {Coin1, Coin2}, {Gold1, Gold2}, {BGold1, BGold2}, {FGold1, FGold2}, Ref, Lv, VipLv) ->
    case erase({advance_payment, Ref}) of
        {CostList, ConsumeId, About} ->
            RealCostList = calc_real_cost_list(CostList, Coin1 - Coin2, Gold1 - Gold2, BGold1 - BGold2, FGold1 - FGold2),
            keep_accounts(RoleId, Ref, RealCostList, ConsumeId, About, [Coin2, Gold2, BGold2, FGold2, Lv, VipLv]);
        _ ->
            ?ERR("miss advance_payment data ~p~n", [[RoleId, {Coin1, Coin2}, {Gold1, Gold2}, {BGold1, BGold2}, Ref]])
    end.

calc_real_cost_list(List, 0, 0, 0, 0) -> List;
calc_real_cost_list(List, Coin, Gold, BGold, FGold) ->
    OtherList = [Item || {Type, _, _} = Item <- List, Type =/= ?TYPE_GOLD andalso Type =/= ?TYPE_BGOLD andalso Type =/= ?TYPE_FGOLD andalso Type =/= ?TYPE_COIN],
    MoneyList = [Item || {_, _, Num} = Item <- [{?TYPE_COIN, 0, Coin}, {?TYPE_GOLD, 0, Gold}, {?TYPE_BGOLD, 0, BGold}, {?TYPE_FGOLD, 0, FGold}], Num > 0],
    MoneyList ++ OtherList.

keep_accounts(RoleId, Ref, RealCostList, ConsumeId, About, OtherData) ->
    Accounts
    = case get(advance_payment_account) of
        undefined ->
            #{};
        Data ->
            Data
    end,
    NewAccounts = Accounts#{Ref => [RoleId, RealCostList, ConsumeId, About, OtherData]},
    put(advance_payment_account, NewAccounts).

%% 消费记录预付款 应用于某些操作可能不成功而需要返回的
%% 目前必须在玩家进程执行
%% 消耗里面必须要有钻石、绑钻或者金币才会有预付款
%% 否则该接口无效
%% return {false, Code, PS} |
%% {true, NewPS, Ref} Ref是一个二进制字符串，长度为使用varchar(63) |
%% {false, Code}
%% 该消耗不会马上参与消费活动，直到调用方执行了 advance_payment_done(RoleId, Ref)消费生效
%% 或者advance_payment_fail(RoleId, Ref, CallBack) %% 失败处理 Callback = {M, F, A}, M:F(CostList, A)
advance_cost_objects(PS, ObjectList, Type, About, AutoBuy) ->
    % CostList = [{Type, GoodsTypeId, GoodsNum}||{ {Type, GoodsTypeId}, GoodsNum} <- lib_goods_util:trans_object_list(ObjectList)],
    case lib_goods_api:check_object_list(PS, ObjectList) of
        {false, ErrorCode} ->
            case ErrorCode =:= ?GOODS_NOT_ENOUGH andalso AutoBuy of
                true ->
                    case lib_goods_api:calc_auto_buy_list(PS, ObjectList) of
                        {ok, NewObjectList} ->
                            case lib_goods_api:check_object_list(PS, NewObjectList) of
                                true ->
                                    do_advance_cost_objects(PS, NewObjectList, Type, About);
                                Err ->
                                    Err
                            end;
                        _A ->
                            {false, ErrorCode}
                    end;
                _ ->
                    {false, ErrorCode}
            end;
        true ->
            do_advance_cost_objects(PS, ObjectList, Type, About);
        Res ->
            Res
    end.

do_advance_cost_objects(PS, CostList, ConsumeType, About) ->
    <<_:32, Ref/binary>> = util:term_to_bitstring(make_ref()),
    ConsumeId = data_goods:get_consume_type(ConsumeType),
    put({advance_payment, Ref}, {CostList, ConsumeId, About}),
    case lib_goods_api:cost_object_list(PS, CostList, {advance_payment, Ref}, util:term_to_string(ConsumeType)) of
        {true, NewPS} ->
            {true, NewPS, Ref};
        Err ->
            erase({advance_payment, Ref}),
            Err %% {false, Res, PS}
    end.

advance_payment_done(RoleId, Ref) ->
    PlayerPid = misc:get_player_process(RoleId),
    Self = self(),
    if
        PlayerPid =:= Self ->
            case get(advance_payment_account) of
                #{Ref := [RoleId, RealCostList, ConsumeId, About, OtherData]} = Accounts ->
                    finish_advance_payment(RoleId, RealCostList, ConsumeId, About, OtherData),
                    put(advance_payment_account, Accounts#{Ref => done});
                undefined ->
                    case load_advance_account(Ref) of
                        {ok, [RoleId, RealCostList, ConsumeId, About, OtherData]} ->
                            delete_advance_account(Ref),
                            finish_advance_payment(RoleId, RealCostList, ConsumeId, About, OtherData);
                        _ ->
                            ok
                    end,
                    put(advance_payment_account, #{Ref => done});
                _ ->
                    ok
            end;
        true ->
            case misc:is_process_alive(PlayerPid) of
                true ->
                    lib_player:apply_cast(PlayerPid, ?APPLY_CAST, ?MODULE, advance_payment_done, [RoleId, Ref]);
                _ ->
                    case load_advance_account(Ref) of
                        {ok, [RoleId, RealCostList, ConsumeId, About, OtherData]} ->
                            delete_advance_account(Ref),
                            finish_advance_payment(RoleId, RealCostList, ConsumeId, About, OtherData);
                        _ ->
                            ok
                    end
            end
    end.


load_advance_account("") -> undefined;
load_advance_account(Ref) ->
    SQL = io_lib:format("SELECT `role_id`, `cost_list`, `consume_id`, `about`, `other` FROM `advance_payment_account` WHERE `ref` = '~s'", [Ref]),
    case db:get_row(SQL) of
        [RoleId, CostListStr, ConsumeId, About, OtherStr] ->
            {ok, [RoleId, util:bitstring_to_term(CostListStr), ConsumeId, About, util:bitstring_to_term(OtherStr)]};
        _ ->
            undefined
    end.

finish_advance_payment(RoleId, RealCostList, ConsumeId, About, OtherData) ->
    F = fun
        ({?TYPE_COIN, _, Num}, {Gold, Coin, BGold, Fgold}) ->
            {Gold, Coin + Num, BGold, Fgold};
        ({?TYPE_BGOLD, _, Num}, {Gold, Coin, BGold, Fgold}) ->
            {Gold, Coin, BGold + Num, Fgold};
        ({?TYPE_FGOLD, _, Num}, {Gold, Coin, BGold, Fgold}) ->
            {Gold, Coin, BGold, Fgold + Num};
        ({?TYPE_GOLD, _, Num}, {Gold, Coin, BGold, Fgold}) ->
            {Gold + Num, Coin, BGold, Fgold};
        (_, Acc) -> Acc %% 只记录3种货币
    end,
    {Gold, Coin, BGold, FGold} = lists:foldl(F, {0, 0, 0, 0}, RealCostList),
    [CoinLeft, GoldLeft, BGoldLeft, FGoldLeft, RoleLv, VipLv] = OtherData,
    NowTime = utime:unixtime(),
    if
        Coin > 0 ->
            mod_log:add_log(log_consume_coin, [
                                               NowTime, ConsumeId, RoleId, 0, 0, Coin, CoinLeft, ulists:concat([About, "(", advance_payment, ")"]), RoleLv, VipLv]);
        Gold > 0 orelse BGold > 0 ->
            mod_log:add_log(log_consume_gold, [
                                               NowTime, ConsumeId, RoleId, 0, 0, Gold, BGold, FGold, GoldLeft, BGoldLeft, FGoldLeft, ulists:concat([About, "(", advance_payment, ")"]), RoleLv, VipLv]);
        true ->
            skip
    end,
    case self() =:= misc:get_player_process(RoleId) of
        true ->
            update_money_consume(RoleId, Gold, BGold, Coin);
        _ ->
            ok
    end.

advance_payment_fail(RoleId, Ref, CallBack) ->
    PlayerPid = misc:get_player_process(RoleId),
    Self = self(),
    if
        PlayerPid =:= Self ->
            Res
            = case get(advance_payment_account) of
                #{Ref := [RoleId, RealCostList|_]} = Accounts ->
                    put(advance_payment_account, Accounts#{Ref => fail}),
                    RealCostList;
                undefined ->
                    put(advance_payment_account, #{Ref => fail}),
                    case load_advance_account(Ref) of
                        {ok, [RoleId, RealCostList|_]} ->
                            delete_advance_account(Ref),
                            RealCostList;
                        _ ->
                            []
                    end;
                _ ->
                    []
            end,
            advance_payment_fail_callback(CallBack, Res);
        true ->
            case misc:is_process_alive(PlayerPid) of
                true ->
                    lib_player:apply_cast(PlayerPid, ?APPLY_CAST, ?MODULE, advance_payment_fail, [RoleId, Ref, CallBack]);
                _ ->
                    Res
                    = case load_advance_account(Ref) of
                        {ok, [RoleId, RealCostList|_]} ->
                            delete_advance_account(Ref),
                            RealCostList;
                        _ ->
                            []
                    end,
                    advance_payment_fail_callback(CallBack, Res)
            end
    end.

advance_payment_fail_callback({Mod, Fun, Args}, Res) ->
    Mod:Fun(Res, Args);

advance_payment_fail_callback(_, _) -> ok.

delete_advance_account(Ref) ->
    SQL = io_lib:format("DELETE FROM `advance_payment_account` WHERE `ref`= '~s'", [Ref]),
    db:execute(SQL).

save_advance_payments(RoleId) ->
    case get(advance_payment_account) of
        undefined ->
            ok;
        Accounts ->
            SaveList
            = maps:fold(fun
                (Ref, Data, Acc) ->
                    case Data of
                        [RoleId, RealCostList, ConsumeId, About, OtherData] ->
                            CostListStr = util:term_to_string(RealCostList),
                            OtherDataStr = util:term_to_string(OtherData),
                            [io_lib:format("('~s', ~p, '~s', ~p, '~s', '~s')", [Ref, RoleId, CostListStr, ConsumeId, About, OtherDataStr])|Acc];
                        _ ->
                            Acc
                    end
            end, [], Accounts),
            case SaveList of
                [] ->
                    skip;
                _ ->
                    SQL = io_lib:format("INSERT INTO `advance_payment_account`(`ref`, `role_id`, `cost_list`, `consume_id`, `about`, `other`) VALUES ~s", [ulists:list_to_string(SaveList, ",")]),
                    db:execute(SQL)
            end
    end.

