%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 团购活动管理器
%%% @end
%%% -------------------------------------------------------
-module(group_buy_mgr).

-behaviour(gen_server).

%% API
-export([
        start_link/0
        ,query_buy_info/2
        ,notice_buyer_add/3
    ]).

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

%% 活动状态变更管理接口
-export([
        act_start/1
        ,act_end/1
    ]).
%% for debug
-compile(export_all).

-include("common.hrl").
-include("custom_act.hrl").
-include("group_buy.hrl").

-define(SERVER, ?MODULE).

%% 增加虚拟购买人数的定时器间隔 单位: 秒
-define(FIX_TIMER_INTERVAL, 15 * 60).
% -define(FIX_TIMER_INTERVAL, 30).   % 调试定时器问题使用

%% 保底阶段数量（低于此值时自动补全，用于计算时间分段）
-define(STAGE_NUM, 5).

%% 进程状态
-record(buy_proc_state, {
        %% 团购相关id列表
        buy_id_list = []

        %% 检查修正人数的定时器
        %% 通常为常驻定时器，用于触发活动各档次各阶段子定时器的初始创建等
        ,fake_timer = undefined

        %% 各活动虚拟人数管理子定时器（处理完该档次人数增长目标则销毁）
        %% 数据格式： [{BuyId, Timer}]
        ,fix_timers = []
    }).

%% 活动配置 - 逻辑运算时的临时参数结构
-record(buy_tmp_cfg, {
        buy_id = {0, 0}
        ,gold_type = 1              % 对应奖励配置中的gold_type
        ,original_price = 999999    % 对应奖励配置中的original_price
        ,discount = {}              % 对应奖励配置中的discount
        ,buyer_need = {}            % 对应奖励配置中的member
        ,fake = {}                  % 对应奖励配置中的fake
        ,stime = 0
        ,etime = 0
    }).

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

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

%% 查询购买信息
-spec query_buy_info(SubType, GradeInfo) -> BuyInfoGradeList when
    SubType :: non_neg_integer(),
    GradeInfo :: GradeId | [GradeId], GradeId :: non_neg_integer(),
    BuyInfoGradeList :: [BuyInfoGrade | {error, BuyId}],
    BuyInfoGrade :: #buy_info_grade{},
    BuyId :: {SubType, GradeId}.
query_buy_info(SubType, GradeId) when is_integer(GradeId) ->
    gen_server:call(?SERVER, {query_buy_info, SubType, [GradeId]});
query_buy_info(_SubType, []) ->
    [];
query_buy_info(SubType, GradeIdList) ->
    gen_server:call(?SERVER, {query_buy_info, SubType, GradeIdList}).

%% 购买更新信息
notice_buyer_add(SubType, GradeId, RoleId) ->
    gen_server:cast(?SERVER, {notice_buyer_add, SubType, GradeId, RoleId}).

%%%===================================================================
%%% 活动状态变更管理接口
%%%===================================================================

%% 活动开放
act_start(SubType) ->
    gen_server:cast(?SERVER, {act_start, SubType}).

%% 活动结束
act_end(SubType) ->
    gen_server:cast(?SERVER, {act_end, SubType}).

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

init([]) ->
    process_flag(trap_exit, true),
    NowTime = utime:unixtime(),
    OpenActList = lib_custom_act_util:get_open_subtype_list(?CUSTOM_ACT_TYPE_GROUP_BUY, NowTime),
    BuyTmpCfgList = to_open_buy_tmp_cfg_list(OpenActList, []),
    DbDataList = load_from_db(),
    #{
        keep_open_ids := KeepOpenIdList
        ,new_open_ids := NewOpenIdList
        ,now_open_data := GroupBuyInfoList
        % ,invalid_data := InvalidDataList
        % ,data_list := RemainDataList
    } = check_keep_open_ids(DbDataList, BuyTmpCfgList),
    BuyIdList = NewOpenIdList ++ KeepOpenIdList,
    put_group_buy_info_list(GroupBuyInfoList),
    % ?INFO("open act list:~w~n"
    %     "BuyTmpCfgList:~w~n"
    %     "DbDataList:~w~n"
    %     "GroupBuyInfoList:~w~n",
    %     [OpenActList, BuyTmpCfgList, DbDataList, GroupBuyInfoList]
    % ),
    FakeTimer = calc_fake_fix(GroupBuyInfoList, NowTime),
    FixTimers = calc_fix_timers(GroupBuyInfoList, NowTime, []),
    %% 有新开的活动则重写一遍
    case NewOpenIdList of
        [_|_] ->
            NewOpenGroupBuyInfoList = get_group_buy_info_list(NewOpenIdList),
            db_save_group_buy_data(NewOpenGroupBuyInfoList);
        _ -> ignore
    end,
    {ok, #buy_proc_state{
            buy_id_list = BuyIdList
            ,fake_timer = FakeTimer
            ,fix_timers = FixTimers
        }}.

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

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

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

terminate(_Reason, _State) ->
    ok.

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

%%%===================================================================
%%% Callback internal functions
%%%===================================================================

%% 查询购买信息
do_handle_call({query_buy_info, SubType, GradeIdList}, _From, State) ->
    #buy_proc_state{
        buy_id_list = BuyIdList
    } = State,
    BuyInfoGradeList = [
        check_to_buy_info_grade(SubType, GradeId, BuyIdList) ||
        GradeId <- GradeIdList
    ],
    {reply, BuyInfoGradeList, State};
do_handle_call(_Request, _From, State) ->
    {reply, error, State}.

%% 更新购买记录（是否要改造为定期集中存储）
do_handle_cast({notice_buyer_add, SubType, GradeId, RoleId}, State) ->
    #buy_proc_state{
        buy_id_list = BuyIdList
    } = State,
    BuyId = {SubType, GradeId},
    case lists:member(BuyId, BuyIdList) of
        true  ->
            case erlang:get(BuyId) of
                #group_buy_info{
                    real_buyer_count = RealBuyerCount
                    ,fix_buyer_count = FixBuyerCount
                    ,buyers = Buyers
                    ,original_price = OriginalPrice
                    ,gold_type = GoldType
                    ,discount_map = DiscountMap
                    ,buyer_count_stage = BuyerCountStage
                    ,stage_total = StageTotal
                } = GroupBuyInfo ->
                    NewBuyers = [RoleId | Buyers],
                    NewFixBuyerCount = FixBuyerCount + 1,
                    NewRealBuyerCount = RealBuyerCount + 1,
                    NewGroupBuyInfo = GroupBuyInfo#group_buy_info{
                        real_buyer_count = NewRealBuyerCount
                        ,fix_buyer_count = NewFixBuyerCount
                        ,buyers = NewBuyers
                    },
                    erlang:put(BuyId, NewGroupBuyInfo),
                    %% TODO 优化db的写入管理
                    db_save_group_buy_data(NewGroupBuyInfo),
                    OldStage = find_stage(FixBuyerCount, BuyerCountStage),
                    NowStage = find_stage(NewFixBuyerCount, BuyerCountStage),
                    case OldStage =/= NowStage of
                        true  ->
                            BuyInfoGrade = to_buy_info_grade(
                                BuyId, NewRealBuyerCount, NewFixBuyerCount,
                                DiscountMap, BuyerCountStage, StageTotal,
                                OriginalPrice, GoldType
                            ),
                            proc_lib:spawn_link(fun() ->
                                        group_buy_api:notice_act_grade_change(BuyInfoGrade)
                                end);
                        false -> ignore
                    end;
                _ ->
                    %% 异常，有人购买，但是没有找到数据
                    ?ERR("Anyone buy, but no data: ~w", [{SubType, GradeId, RoleId}]),
                    ignore
            end;
        false ->
            ?ERR("Anyone buy, but no data: ~w", [{SubType, GradeId, RoleId}]),
            ignore
    end,
    {noreply, State};

%% 活动开放管理
do_handle_cast({act_start, SubType}, State) ->
    #buy_proc_state{
        buy_id_list = BuyIdList
        ,fake_timer = FakeTimer
        ,fix_timers = FixTimers
    } = State,
    %% 获取到配置
    case lib_custom_act_util:get_custom_act_open_info(?CUSTOM_ACT_TYPE_GROUP_BUY, SubType) of
        ActInfo = #act_info{} ->
            BuyTmpCfgList = to_open_buy_tmp_cfg_list([ActInfo], []),
            SubTypeBuyIdList = [
                BuyId ||
                BuyId = {SubTypeX, _GradeIdX} <- BuyIdList,
                SubTypeX =:= SubType
            ],
            SubTypeGroupBuyInfoList = get_group_buy_info_list(SubTypeBuyIdList),
            #{
                keep_open_ids := _KeepOpenIdList
                ,new_open_ids := NewOpenIdList
                ,now_open_data := GroupBuyInfoList
                ,invalid_data  := InvalidDataList
                ,data_list := RemainDataList
            } = check_keep_open_ids(SubTypeGroupBuyInfoList, BuyTmpCfgList),
            DelIdList =
            case InvalidDataList =/= [] orelse RemainDataList =/= [] of
                true  ->
                    %% TODO 可能属于运营配置错误热更修正，目前不予结算返还处理，是否再考虑？
                    ?INFO("活动开放变更，导致失效的数据:~p~n"
                        "活动开放变更，导致直接结束的数据:~p",
                        [InvalidDataList, RemainDataList]
                    ),
                    [
                        BuyId || #group_buy_info{buy_id = BuyId} <- InvalidDataList
                    ] ++ [
                        BuyId || #group_buy_info{buy_id = BuyId} <- RemainDataList
                    ];
                false ->
                    []
            end,
            %% DelIdList中的BuyId，可能出现在NewOpenIdList中
            %% （即DelIdList中含有变更但未关闭的活动）
            RealDelIdList = DelIdList -- NewOpenIdList,
            lists:foreach(
                fun(BuyId) ->
                        erlang:erase(BuyId)
                end,
                RealDelIdList
            ),
            NewBuyIdList = (BuyIdList -- DelIdList) ++ NewOpenIdList,
            put_group_buy_info_list(GroupBuyInfoList),
            %% 写入到数据库
            delete_invalid_data(RealDelIdList),
            db_save_group_buy_data(GroupBuyInfoList),
            %% 重新计算fake_timer（收到消息时会执行取消旧定时器操作）
            self() ! {timeout, FakeTimer, fake_timer},
            %% 清除掉相关fix_timer
            NewFixTimers = clear_fix_timers(FixTimers, DelIdList),
            NewState = State#buy_proc_state{
                buy_id_list = NewBuyIdList
                ,fix_timers = NewFixTimers
            },
            {noreply, NewState};
        _ ->
            %% 活动未开放
            {noreply, State}
    end;

%% 活动关闭管理
do_handle_cast({act_end, SubType}, State) ->
    #buy_proc_state{
        buy_id_list = BuyIdList
        ,fix_timers = FixTimers
    } = State,
    DelIdList = [
        BuyId ||
        BuyId = {SubTypeX, _GradeIdX} <- BuyIdList,
        SubTypeX =:= SubType
    ],
    GroupBuyInfoList = get_group_buy_info_list(DelIdList),
    % ?INFO("act_end, del:~w", [GroupBuyInfoList]),
    %% 结算折扣返还
    proc_lib:spawn_link(
        fun() ->
                notice_check_return(GroupBuyInfoList)
        end),
    %% 清理数据
    erase_group_buy(DelIdList),
    NewBuyIdList = BuyIdList -- DelIdList,
    %% 清除掉相关fix_timer
    NewFixTimers = clear_fix_timers(FixTimers, DelIdList),
    NewState = State#buy_proc_state{
        buy_id_list = NewBuyIdList
        ,fix_timers = NewFixTimers
    },
    {noreply, NewState};

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

do_handle_info({timeout, _Timer, fake_timer}, State) ->
    #buy_proc_state{
        buy_id_list = BuyIdList
        ,fake_timer = OldTimer
        ,fix_timers = FixTimers
    } = State,
    util:cancel_timer(OldTimer),
    NowTime = utime:unixtime(),
    GroupBuyInfoList = get_group_buy_info_list(BuyIdList),
    NewFakeTimer = calc_fake_fix(GroupBuyInfoList, NowTime),
    NewFixTimers = calc_fix_timers(GroupBuyInfoList, NowTime, FixTimers),
    % ?INFO("OldFixTimers:~w, NewFixTimers:~w", [FixTimers, NewFixTimers]),
    NewState = State#buy_proc_state{
        fake_timer = NewFakeTimer
        ,fix_timers = NewFixTimers
    },
    {noreply, NewState};

do_handle_info({timeout, _Timer, {fix_timer, BuyId}}, State) ->
    #buy_proc_state{
        fix_timers = OldFixTimers
    } = State,
    FixTimers =
    case lists:keytake(BuyId, 1, OldFixTimers) of
        {value, {_, OldTimer}, RemainFixTimers} ->
            util:cancel_timer(OldTimer),
            RemainFixTimers;
        _ ->
            OldFixTimers
    end,
    NewFixTimer = do_fix_buyer_count(BuyId),
    NewFixTimers =
    case NewFixTimer == false of
        true  -> FixTimers;
        false -> [{BuyId, NewFixTimer} | FixTimers]
    end,
    % ?INFO("fix_timer,BuyId:~w, NewFixTimers:~w, OldFixTimers:~w", [BuyId, NewFixTimers, OldFixTimers]),
    NewState = State#buy_proc_state{
        fix_timers = NewFixTimers
    },
    {noreply, NewState};

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

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

%% -------------------------------------------------------
%% 数据加载 及 配置验证转换管理
%% -------------------------------------------------------
load_from_db() ->
    Sql = ?SQL_SELECT_GROUP_BUY_DATA,
    db:get_all(Sql).

to_open_buy_tmp_cfg_list([
        #act_info{key = {Type, SubType}, stime = ActSTime, etime = ActETime} | OpenActList
    ], Acc) ->
    RewardCfgList = lib_custom_act_util:get_act_reward_cfg_list(Type, SubType),
    NewAcc = lists:foldl(fun(RewardCfg, TmpCfgAcc) ->
                case to_buy_tmp_cfg(SubType, RewardCfg, ActSTime, ActETime) of
                    #buy_tmp_cfg{} = BuyTmpCfg ->
                        [BuyTmpCfg | TmpCfgAcc];
                    _ ->
                        TmpCfgAcc
                end
        end, Acc, RewardCfgList),
    to_open_buy_tmp_cfg_list(OpenActList, NewAcc);
to_open_buy_tmp_cfg_list([], Acc) ->
    Acc.

%% 从活动奖励配置中，取出此活动的各项条件配置
to_buy_tmp_cfg(SubType, #custom_act_reward_cfg{
        grade = GradeId, condition = Condition
    }, ActSTime, ActETime) ->
    CondResult = lib_custom_act_check:check_act_condtion([gold_type, original_price, discount, member, fake], Condition),
    % ?INFO("cond result:~w", [CondResult]),
    case CondResult of
        [GoldType, OriginalPrice, Discount, BuyerNeed, Fake] ->
            #buy_tmp_cfg{
                buy_id = {SubType, GradeId}
                ,gold_type = GoldType
                ,original_price = OriginalPrice
                ,discount = Discount
                ,buyer_need = BuyerNeed
                ,fake = Fake
                ,stime = ActSTime
                ,etime = ActETime
            };
        _False ->
            false
    end.

check_keep_open_ids(DataList, BuyTmpCfgList) ->
    %% data_list为两类数据格式，
    %% 一、从数据库中load出来的列表数据
    %% 二、当前进程数据中的活动group_buy_info数据
    InitState = #{
        keep_open_ids => []     % 保持开放的
        ,new_open_ids => []     % 新增开放的
        ,now_open_data => []    % 开放活动的数据
        ,invalid_data => []     % 失效的数据（原本开放，因配置变更失效）
        ,data_list => DataList  % 原始数据（最终返回的剩余部分也意味着失效，无配置）
    },
    lists:foldl(fun verify_data/2, InitState, BuyTmpCfgList).

%% 检验DB中数据的有效性
%% DB中取出的数据，不检验时间信息，由活动中的时间信息决定是否有效
%%
%% invalid_data中为失效数据（即活动前后配置不一致）
%% data_list中为当前未开放的历史数据（暂时不处理）
verify_data(BuyTmpCfg, AccState) ->
    #buy_tmp_cfg{
        buy_id = {SubType, GradeId}
        ,gold_type = GoldTypeCfg
        ,original_price = OriginalPriceCfg
    } = BuyTmpCfg,
    #{
        keep_open_ids := KeepOpenIdList
        ,new_open_ids := NewOpenIdList
        ,data_list := DataList
        ,now_open_data := NowOpenDataList
        ,invalid_data := InvalidDataList
    } = AccState,
    BuyId = {SubType, GradeId},
    case take_open_data(SubType, GradeId, DataList, []) of
        {value, Data, Others} ->
            [
                SubType, GradeId, RealBuyerCount, FixBuyerCount,
                GoldType, OriginalPrice, _STime, _ETime, BuyersData
            ] = get_buy_data(Data),
            if
                OriginalPrice =:= OriginalPriceCfg andalso
                GoldType =:= GoldTypeCfg ->
                    %% 原数据有效（时间直接更新为当前的配置）
                    BuyInfo = {RealBuyerCount, FixBuyerCount, BuyersData},
                    GroupBuyInfo = to_group_buy_info(BuyId, BuyInfo, BuyTmpCfg),
                    AccState#{
                        keep_open_ids => [BuyId | KeepOpenIdList]
                        ,now_open_data => [GroupBuyInfo | NowOpenDataList]
                        ,data_list => Others
                    };
                true ->
                    %% 数据失效（正常不应该配置前后对不上）
                    BuyInfo = {0, 0, []},
                    GroupBuyInfo = to_group_buy_info(BuyId, BuyInfo, BuyTmpCfg),
                    AccState#{
                        new_open_ids => [BuyId | NewOpenIdList]
                        ,now_open_data => [GroupBuyInfo | NowOpenDataList]
                        ,data_list => Others
                        ,invalid_data => [Data | InvalidDataList]
                    }
            end;
        _False ->
            %% 无对应的历史数据
            BuyInfo = {0, 0, []},
            GroupBuyInfo = to_group_buy_info(BuyId, BuyInfo, BuyTmpCfg),
            AccState#{
                new_open_ids => [BuyId | NewOpenIdList]
                ,now_open_data => [GroupBuyInfo | NowOpenDataList]
            }
    end.

get_buy_data(Data = [
        _SubType, _GradeId, _RealBuyerCount, _FixBuyerCount,
        _GoldType, _OriginalPrice, _STime, _ETime, _BuyersBin
    ]) -> Data;
get_buy_data(#group_buy_info{
        buy_id = {SubType, GradeId}
        ,real_buyer_count = RealBuyerCount
        ,fix_buyer_count = FixBuyerCount
        ,gold_type = GoldType
        ,original_price = OriginalPrice
        ,stime = STime
        ,etime = ETime
        ,buyers = Buyers
    }) ->
    [
        SubType, GradeId, RealBuyerCount, FixBuyerCount,
        GoldType, OriginalPrice, STime, ETime, Buyers
    ].

calc_discount_map(Discount, BuyerNeed) ->
    MinStageCount = ?STAGE_NUM,
    if
        not is_tuple(Discount) ->
            StageList = lists:seq(1, MinStageCount),
            CfgList = lists:duplicate(MinStageCount, {10, 0}),
            maps:from_list(lists:zip(StageList, CfgList));
        not is_tuple(BuyerNeed) ->
            StageList = lists:seq(1, MinStageCount),
            CfgList = lists:duplicate(MinStageCount, {10, 0}),
            maps:from_list(lists:zip(StageList, CfgList));
        true ->
            %% 运营如果配置折扣非大到小，人数非小到大，
            %% 在技术上也是允许的，这里不忽略该配置
            DiscountList = tuple_to_list(Discount),
            BuyerNeedList = tuple_to_list(BuyerNeed),
            to_discount_map(DiscountList, BuyerNeedList, 1, MinStageCount, #{})
    end.

to_discount_map([Discount | DiscountList], [BuyerNeed | BuyerNeedList], Stage, MinStageCount, Acc) ->
    NewAcc = Acc#{
        Stage => {Discount, BuyerNeed}
    },
    to_discount_map(DiscountList, BuyerNeedList, Stage + 1, MinStageCount, NewAcc);
to_discount_map([Discount | DiscountList], [], Stage, MinStageCount, Acc) when Stage =< MinStageCount ->
    {_, BuyerNeed} = maps:get(Stage - 1, Acc, 0),
    NewAcc = Acc#{
        Stage => {Discount, BuyerNeed}
    },
    to_discount_map(DiscountList, [], Stage + 1, MinStageCount, NewAcc);
to_discount_map([], [BuyerNeed | BuyerNeedList], Stage, MinStageCount, Acc) when Stage =< MinStageCount ->
    {Discount, _} = maps:get(Stage - 1, Acc, 10),
    NewAcc = Acc#{
        Stage => {Discount, BuyerNeed}
    },
    to_discount_map([], BuyerNeedList, Stage + 1, MinStageCount, NewAcc);
to_discount_map([], [], Stage, MinStageCount, Acc) when Stage =< MinStageCount ->
    DiscountNeed = maps:get(Stage - 1, Acc, {10, 0}),
    NewAcc = Acc#{
        Stage => DiscountNeed
    },
    to_discount_map([], [], Stage + 1, MinStageCount, NewAcc);
to_discount_map(_, _, _, _, Acc) ->
    Acc.

to_group_buy_info(
    BuyId = {_, _},
    {RealBuyerCount, FixBuyerCount, BuyersData},
    #buy_tmp_cfg{
        gold_type = GoldType
        ,original_price = OriginalPrice
        ,discount = DiscountCfg
        ,buyer_need = BuyerNeedCfg
        ,fake = FakeCfg
        ,stime = STime
        ,etime = ETime
    }) ->
    Buyers =
    case is_list(BuyersData) of
        true  -> BuyersData;
        false ->
            case util:bitstring_to_term(BuyersData) of
                [_|_] = BuyerList -> BuyerList;
                _ -> []
            end
    end,
    DiscountMap = calc_discount_map(DiscountCfg, BuyerNeedCfg),
    StageList = maps:keys(DiscountMap),
    StageTotal = case StageList of
        [_|_] -> lists:max(StageList);
        _ -> 0
    end,
    #group_buy_info{
        buy_id = BuyId
        %% 活动数据部分
        ,real_buyer_count = RealBuyerCount
        ,fix_buyer_count = FixBuyerCount
        ,buyers = Buyers

        %% 配置验证转换后的中间状态部分
        ,discount_map = DiscountMap
        ,buyer_count_stage = to_buyer_count_stage(DiscountMap)
        ,stage_total = StageTotal

        %% 配置部分
        ,gold_type = GoldType
        ,original_price = OriginalPrice
        ,discount = DiscountCfg
        ,buyer_need = BuyerNeedCfg
        ,fake = FakeCfg
        ,stime = STime
        ,etime = ETime
    }.

%% 有序列表 BuyerCount由小到大[{BuyerCount, State}]
%% 用于查询当前人数最适合的折扣阶段，再反过来映射到折扣值
to_buyer_count_stage(DiscountMap) ->
    List = maps:to_list(DiscountMap),
    BuyerCountStageMap = lists:foldl(fun({Stage, {_, BuyerNeed}}, Acc) ->
                case maps:get(BuyerNeed, Acc, undefined) of
                    OldStage when is_integer(OldStage) andalso OldStage >= Stage ->
                        Acc;
                    _Other ->
                        Acc#{BuyerNeed => Stage}
                end
        end, #{}, List),
    lists:sort(maps:to_list(BuyerCountStageMap)).

%% 从db数据中取出开放的活动数据
%% data_list为两类数据格式
%% 一、从数据库中load出来的列表数据
%% 二、当前进程数据中的活动group_buy_info数据
take_open_data(SubType, GradeId, [
        GroupBuyData = #group_buy_info{buy_id = {SubType, GradeId}} |
        Remain
    ], Acc) ->
    {value, GroupBuyData, Remain ++ Acc};
take_open_data(SubType, GradeId, [[SubType, GradeId | _] = DbData | Remain], Acc) ->
    {value, DbData, Remain ++ Acc};
take_open_data(SubType, GradeId, [Data | Remain], Acc) ->
    take_open_data(SubType, GradeId, Remain, [Data | Acc]);
take_open_data(_SubType, _GradeId, [], _Acc) ->
    false.

%% -------------------------------------------------------
%% 活动折扣阶段查询、购买信息查询等
%% -------------------------------------------------------
find_discount(FixBuyerCount, DiscountMap, BuyerCountStage) ->
    Stage = find_stage(FixBuyerCount, BuyerCountStage),
    find_discount_with_stage(Stage, DiscountMap).

find_discount_with_stage(Stage, DiscountMap) ->
    case maps:get(Stage, DiscountMap) of
        {Discount, _} -> Discount;
        _ -> 10
    end.

find_stage(BuyerCount, BuyerCountStage) ->
    sub_find_stage(BuyerCount, BuyerCountStage, 1).

sub_find_stage(BuyerCount, [{Need, Stage} | Remain], StageAcc) ->
    case BuyerCount >= Need of
        true  ->
            sub_find_stage(BuyerCount, Remain, Stage);
        false ->
            StageAcc
    end;
sub_find_stage(_, [], StageAcc) ->
    StageAcc.

check_to_buy_info_grade(SubType, GradeId, BuyIdList) ->
    BuyId = {SubType, GradeId},
    case lists:member(BuyId, BuyIdList) of
        true  ->
            case erlang:get(BuyId) of
                #group_buy_info{
                    real_buyer_count = RealBuyerCount
                    ,fix_buyer_count = FixBuyerCount
                    ,discount_map = DiscountMap
                    ,buyer_count_stage = BuyerCountStage
                    ,original_price = OriginalPrice
                    ,gold_type = GoldType
                    ,stage_total = StageTotal
                } ->
                    to_buy_info_grade(
                        BuyId, RealBuyerCount, FixBuyerCount,
                        DiscountMap, BuyerCountStage, StageTotal,
                        OriginalPrice, GoldType
                    );
                _ ->
                    {error, BuyId}
            end;
        false ->
            {error, BuyId}
    end.

to_buy_info_grade(BuyId, RealBuyerCount, FixBuyerCount,
    DiscountMap, BuyerCountStage, StageTotal,
    OriginalPrice, GoldType) ->
    Stage = find_stage(FixBuyerCount, BuyerCountStage),
    StageNoFix = find_stage(RealBuyerCount, BuyerCountStage),
    Discount = find_discount_with_stage(Stage, DiscountMap),
    #buy_info_grade{
        buy_id = BuyId
        ,real_buyer_count = RealBuyerCount
        ,fix_buyer_count = FixBuyerCount
        ,discount = Discount
        ,original_price = OriginalPrice
        ,gold_type = GoldType
        ,stage = Stage
        ,stage_no_fix = StageNoFix
        ,stage_total = StageTotal
    }.

%% 从进程字典中取出指定BuyIdList对应的活动数据
%% 通常是取全部数据
get_group_buy_info_list(BuyIdList) ->
    lists:foldl(fun(BuyId, Acc) ->
                case erlang:get(BuyId) of
                    #group_buy_info{} = GroupBuyInfo ->
                        [GroupBuyInfo | Acc];
                    _ ->
                        Acc
                end
        end, [], BuyIdList).

%% -------------------------------------------------------
%% 数据存储、更新
%% -------------------------------------------------------
db_save_group_buy_data(GroupBuyInfo = #group_buy_info{}) ->
    db_save_group_buy_data([GroupBuyInfo]);
db_save_group_buy_data(GroupBuyInfoList) ->
    DataFmtFun = fun fmt_group_buy_data/1,
    udb:save_data_list(GroupBuyInfoList, ?SQL_REPLACE_GROUP_BUY_DATA_HEAD, DataFmtFun).

fmt_group_buy_data(GroupBuyInfo) ->
    #group_buy_info{
        buy_id = {SubType, GradeId}
        ,real_buyer_count = RealBuyerCount
        ,fix_buyer_count = FixBuyerCount
        ,buyers = Buyers
        ,original_price = OriginalPrice
        ,gold_type = GoldType
        ,stime = STime
        ,etime = ETime
    } = GroupBuyInfo,
    BuyersStr = util:term_to_bitstring(Buyers),
    util:fmt_bin("(~w,~w,~w,~w,~w,~w,~w,~w,'~ts')", [
            SubType, GradeId, RealBuyerCount, FixBuyerCount,
            GoldType, OriginalPrice,
            STime, ETime, BuyersStr
        ]).

%% 删除数据
delete_invalid_data([]) -> ok;
delete_invalid_data([{SubType, GradeId}]) ->
    delete_invalid_data_one(SubType, GradeId);
delete_invalid_data(DelIdList = [_|_]) ->
    %% 多条数据放事务中一次性删除
    Fun = fun() ->
            lists:foreach(fun({SubType, GradeId}) ->
                        db:execute(?SQL_DEL_GROUP_BUY_DATA, [SubType, GradeId])
                end, DelIdList)
    end,
    db:transaction(Fun).

delete_invalid_data_one(SubType, GradeId) ->
    db:execute(?SQL_DEL_GROUP_BUY_DATA, [SubType, GradeId]).

put_group_buy_info_list([
        GroupBuyInfo = #group_buy_info{buy_id = BuyId = {_, _}} | GroupBuyInfoList
    ]) ->
    erlang:put(BuyId, GroupBuyInfo),
    put_group_buy_info_list(GroupBuyInfoList);
put_group_buy_info_list([]) ->
    ok.

erase_group_buy([BuyId | BuyIdList]) ->
    erlang:erase(BuyId),
    erase_group_buy(BuyIdList);
erase_group_buy([]) ->
    ok.

%% -------------------------------------------------------
%% 虚拟保底折扣人数维护管理相关接口
%% -------------------------------------------------------

%% 不断查找下一阶配置，直到计算的下一阶段时间符合设定的对比参考值
%% -> true    与原参考时间相等，需要添加到BuyIdFixList
%%    false   未更新原参考时间
%%    NewTime 更新了参考时间，需要清空原来统计的BuyIdFixList，重新统计
calc_next_time_with_refer(Stage, FakeCfg, TupleSize, AllTime, STime, {NowTime, MinTime}) ->
    NextStage = Stage + 1,
    NextStageTime = calc_stage_time(NextStage, FakeCfg, TupleSize, AllTime, STime),
    if
        NextStageTime == false ->
            false;
        NextStageTime > MinTime ->
            false;
        NextStageTime < NowTime ->
            calc_next_time_with_refer(
                NextStage, FakeCfg, TupleSize,
                AllTime, STime,
                {NowTime, MinTime}
            );
        NextStageTime =:= MinTime ->
            true;
        true ->
            NextStageTime
    end.

%% 计算下一阶段的时间值
calc_stage_time(Stage, FakeCfg, TupleSize, AllTime, STime) ->
    case Stage =< TupleSize of
        true  ->
            PercentCfg = element(Stage, FakeCfg),
            STime + util:ceil(AllTime * PercentCfg / 100);
        false ->
            false
    end.

%% 计算虚拟保底人数配置的触发定时器
calc_fake_fix(GroupBuyInfoList, NowTime) ->
    case GroupBuyInfoList of
        [#group_buy_info{etime = ETime} | _] ->
            {
                MinNextTime, _BuyIdFixList
            } = sub_calc_fake_fix(GroupBuyInfoList, NowTime, ETime, []),
            start_timer(MinNextTime - NowTime, fake_timer);
        _ ->
            undefined
    end.

%% 遍历活动各档次的资料，查找下一个可能需要做购买人数虚拟调节的时间点
sub_calc_fake_fix([GroupBuyInfo | RemainList], NowTime, MinNextTime, BuyIdFixList) ->
    #group_buy_info{
        buy_id = BuyId
        ,fix_buyer_count = FixBuyerCount
        ,buyer_count_stage = BuyerCountStage
        ,fake = FakeCfg
        ,stime = STime
        ,etime = ETime
    } = GroupBuyInfo,
    case is_tuple(FakeCfg) of
        true  ->
            Stage = find_stage(FixBuyerCount, BuyerCountStage),
            TupleSize = tuple_size(FakeCfg),
            AllTime = ETime - STime,
            case calc_next_time_with_refer(
                    Stage, FakeCfg, TupleSize,
                    AllTime, STime,
                    {NowTime, MinNextTime}
                ) of
                true  ->    % 与原参考时间相等，需要添加到BuyIdFixList
                    NewBuyIdFixList = [BuyId | BuyIdFixList],
                    sub_calc_fake_fix(RemainList, NowTime, MinNextTime, NewBuyIdFixList);
                false ->    % 未更新原参考时间
                    sub_calc_fake_fix(RemainList, NowTime, MinNextTime, BuyIdFixList);
                NewTime ->  % 更新了参考时间，需要清空原来统计的BuyIdFixList，重新统计
                    sub_calc_fake_fix(RemainList, NowTime, NewTime, [])
            end;
        false ->
            sub_calc_fake_fix(RemainList, NowTime, MinNextTime, BuyIdFixList)
    end;
sub_calc_fake_fix([], _NowTime, MinNextTime, BuyIdFixList) ->
    {MinNextTime, BuyIdFixList}.

calc_fix_timers(GroupBuyInfoList, NowTime, FixTimers) ->
    lists:foldl(fun(GroupBuyInfo, TimerAccList) ->
                sub_calc_fix_timer(GroupBuyInfo, NowTime, TimerAccList)
        end, FixTimers, GroupBuyInfoList).

sub_calc_fix_timer(GroupBuyInfo, NowTime, FixTimers) ->
    #group_buy_info{
        buy_id = BuyId
        ,fix_buyer_count = FixBuyerCount
        ,buyer_count_stage = BuyerCountStage
        ,fake = FakeCfg
        ,stime = STime
        ,etime = ETime
    } = GroupBuyInfo,
    % ?INFO("BuyId:~w sub_calc_fix_timer", [BuyId]),
    TimerExists =
    case lists:keyfind(BuyId, 1, FixTimers) of
        {_, OldFixTimer} ->
            case erlang:read_timer(OldFixTimer) of
                false -> false;
                _RemainMS -> true
            end;
        _ ->
            false
    end,
    case TimerExists of
        true  ->
            % ?INFO("BuyId:~w 已存在FixTimer", [BuyId]),
            FixTimers;
        false ->
            %% 检查是否需要触发，返回最终的定时器列表
            case is_tuple(FakeCfg) of
                true  ->
                    Stage = find_stage(FixBuyerCount, BuyerCountStage),
                    TupleSize = tuple_size(FakeCfg),
                    AllTime = ETime - STime,
                    NextStage = Stage + 1,
                    NextStageTime = calc_stage_time(NextStage, FakeCfg, TupleSize, AllTime, STime),
                    if
                        NextStageTime == false ->   % 没有该阶段时间
                            % ?INFO("没有该阶段时间"),
                            FixTimers;
                        NextStageTime =< NowTime -> % 达成了时间要求，需要新增
                            % ?INFO("达成了时间要求，需要新增"),
                            RemainTime = max(NextStageTime + ?FIX_TIMER_INTERVAL - NowTime, 1),
                            FixTimer = start_timer(RemainTime, {fix_timer, BuyId}),
                            [{BuyId, FixTimer} | FixTimers];
                        true ->
                            % ?INFO("未达成时间要求，暂不新增"),
                            FixTimers
                    end;
                false ->
                    % ?INFO("Fake配置无效"),
                    FixTimers
            end
    end.

start_timer(RemainTime, Msg) ->
    case RemainTime < 0 of
        true  -> undefined;
        false ->
            Timeout = min(?MAX_TIMEOUT, RemainTime * 1000),
            % ?INFO_TTY("RemainTime:~w, timeout:~w", [RemainTime, Timeout]),
            erlang:start_timer(Timeout, self(), Msg)
    end.

do_fix_buyer_count(BuyId) ->
    % ?INFO("do_fix_buyer_count, BuyId:~w, NextStage:~w", [BuyId, _NextStage]),
    case erlang:get(BuyId) of
        #group_buy_info{
            real_buyer_count = RealBuyerCount
            ,fix_buyer_count = FixBuyerCount
            ,buyer_count_stage = BuyerCountStage
            ,discount_map = DiscountMap
            ,gold_type = GoldType
            ,original_price = OriginalPrice
            ,stage_total = StageTotal
            ,fake = FakeCfg
            ,stime = STime
            ,etime = ETime
        } = GroupBuyInfo when is_tuple(FakeCfg) ->
            Stage = find_stage(FixBuyerCount, BuyerCountStage),
            NextStage = Stage + 1,
            TupleSize = tuple_size(FakeCfg),
            AllTime = ETime - STime,
            NextStageTime = calc_stage_time(NextStage, FakeCfg, TupleSize, AllTime, STime),
            NowTime = utime:unixtime(),
            if
                NextStageTime == false ->
                    % ?INFO("NextStageTime == false"),
                    false;
                NextStageTime > NowTime ->
                    % ?INFO("To early"),
                    false;
                true ->
                    ReferCount = element(NextStage, FakeCfg),
                    case FixBuyerCount < ReferCount of
                        true  ->
                            NewFixBuyerCount = FixBuyerCount + 1,
                            NewGroupBuyInfo = GroupBuyInfo#group_buy_info{
                                fix_buyer_count = NewFixBuyerCount
                            },
                            erlang:put(BuyId, NewGroupBuyInfo),
                            %% TODO 暂时真实写库 - 待统一优化
                            db_save_group_buy_data(NewGroupBuyInfo),
                            case NewFixBuyerCount >= ReferCount of
                                true  ->
                                    % ?INFO("NewFixBuyerCount >= ReferCount, 不再需要定时器"),
                                    % 通知玩家折扣变更
                                    BuyInfoGrade = to_buy_info_grade(
                                        BuyId, RealBuyerCount, NewFixBuyerCount,
                                        DiscountMap, BuyerCountStage, StageTotal,
                                        OriginalPrice, GoldType
                                    ),
                                    proc_lib:spawn_link(fun() ->
                                                group_buy_api:notice_act_grade_change(BuyInfoGrade)
                                        end),
                                    false;
                                false ->
                                    % ?INFO("添加虚拟人数后，仍未达到人数参考值，需要再次创建FixTimer"),
                                    start_timer(?FIX_TIMER_INTERVAL, {fix_timer, BuyId})
                            end;
                        false ->
                            % ?INFO("不满足 FixBuyerCount < ReferCount"),
                            false
                    end
            end;
        _ ->
            % ?INFO("缺少数据BuyId:~w", [BuyId]),
            false
    end.

%% 因配置变动的活动过期，清理相关定时器
clear_fix_timers(FixTimers, DelIdList) ->
    lists:foldl(fun(BuyId, FixTimersAcc) ->
                case lists:keytake(BuyId, 1, FixTimersAcc) of
                    {value, {_, FixTimer}, RemainFixTimers} ->
                        util:cancel_timer(FixTimer),
                        RemainFixTimers;
                    _False ->
                        FixTimersAcc
                end
        end, FixTimers, DelIdList).

%% -------------------------------------------------------
%% 结算返还
%% -------------------------------------------------------

%% 活动结束时，折扣返还
%% 返还时，处理流程分为两类：玩家在线与不在线
notice_check_return([
        #group_buy_info{
            buy_id = {SubType, GradeId}
            ,buyers = Buyers
            ,fix_buyer_count = FixBuyerCount
            ,discount_map = DiscountMap
            ,buyer_count_stage = BuyerCountStage
            ,original_price = OriginalPrice
            ,gold_type = GoldType
            ,stime = STime
            ,etime = ETime
        } | GroupBuyInfoList]) ->
    try
        Discount = find_discount(FixBuyerCount, DiscountMap, BuyerCountStage),
        CurrPrice = group_buy_api:count_curr_price(OriginalPrice, Discount),
        % ?INFO("notice_check_return:{~w,~w}, curr price:~w", [SubType, GradeId, CurrPrice]),
        Params = #check_return_params{
            subtype = SubType
            ,grade_id = GradeId
            ,original_price = OriginalPrice
            ,gold_type = GoldType
            ,curr_price = CurrPrice
            ,discount = Discount
            ,stime = STime
            ,etime = ETime
        },
        group_buy_api:check_return(Buyers, Params, 1)
    catch
        ?EXCEPTION(_Class, _Reason, _Stacktrace) ->
            ?ERR("group_buy auto return:~n"
                "SubType:~w,GradeId:~w, error:{~w,~w}~n"
                "~p", [
                    SubType, GradeId, _Class, _Reason,
                    ?GET_STACK(_Stacktrace)
                ]
            )
    end,
    notice_check_return(GroupBuyInfoList);
notice_check_return([_|GroupBuyInfoList]) ->
    notice_check_return(GroupBuyInfoList);
notice_check_return([]) ->
    ok.


