%%%-------------------------------------------------------------------
%%% @author liushl
%%% @doc
%%% 秒杀-跨服管理
%%% 跨服定制活动控制进程会处理好跨服中心和本服的活动开启关闭问题，这里，只处理玩法逻辑即可
%%% 支持玩法多开
%%% @end
%%%-------------------------------------------------------------------
-module(seckill_center).
-author("liushl").

-behaviour(gen_server).

-include("seckill.hrl").
-include("custom_act.hrl").
-include("common.hrl").
-include("errcode.hrl").

%% API
-export([start_link/0]).

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

-export([
    seckill_begin/1                 %% 活动开启
    , seckill_end/1                 %% 活动结束-关闭活动
    , seckill_zero_time/0           %% 0点跨天
    , recalc_zone/1                 %% 重新分区

    , ask_seckill_log/1             %% 查看秒杀记录
    , ask_seckill_info/4            %% 打开秒杀界面
    , ask_seckill_round_info/5      %% 打开秒杀界面-查看各场次数据
    , buy_seckill_goods/5           %% 秒杀-抢购商品

    , apply_cast/1
    , apply_info/1
]).

-define(SERVER, ?MODULE).

%%%===================================================================
%%% API
%%%===================================================================
%% 活动开启
seckill_begin(ActInfo) ->
    ?MODULE ! {seckill_begin, ActInfo}.

%% 活动结束-关闭活动
seckill_end(ActInfo) ->
    ?MODULE ! {seckill_end, ActInfo}.

%% 0点跨天
seckill_zero_time() ->
    ?MODULE ! seckill_zero_time.

%% 重新分区
recalc_zone(#{zone_map := ZoneMap}) ->
    case maps:to_list(ZoneMap) of
        [{OldZoneId, ToZoneId}|_] ->
            ?MODULE ! {recalc_zone, OldZoneId, ToZoneId};
        _ ->
            ignore
    end;
recalc_zone(_) -> ignore.

%% 查看秒杀记录
ask_seckill_log(Sid) ->
    ZoneId = czone_api:env_get_zone_id(),
    clusters_node:apply_cast(?MODULE, apply_info, [{ask_seckill_log, Sid, ZoneId}]).

%% 打开秒杀界面
ask_seckill_info(SubType, Sid, RoleId, Lv) ->
    ZoneId = czone_api:env_get_zone_id(),
    Msg = {ask_seckill_info, Sid, RoleId, Lv},
    clusters_node:apply_cast(?MODULE, apply_cast, [{common, SubType, ZoneId, Msg}]).

%% 打开秒杀界面-查看各场次数据
ask_seckill_round_info(SubType, Sid, RoleId, Lv, Round) ->
    ZoneId = czone_api:env_get_zone_id(),
    Msg = {ask_seckill_round_info, Sid, RoleId, Lv, Round},
    clusters_node:apply_cast(?MODULE, apply_cast, [{common, SubType, ZoneId, Msg}]).

%% 秒杀-抢购商品
buy_seckill_goods(SubType, RoleId, Name, SubType, GradeId) ->
    ZoneId = czone_api:env_get_zone_id(),
    ServerNum = config:get_server_num(),
    Msg = {buy_seckill_goods, node(), RoleId, ServerNum, Name, GradeId},
    clusters_node:apply_cast(?MODULE, apply_cast, [{common, SubType, ZoneId, Msg}]).

apply_cast(Msg) ->
    gen_server:cast(?MODULE, Msg).
apply_info(Msg) ->
    ?MODULE ! Msg.
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @end
%%--------------------------------------------------------------------
-spec(start_link() ->
    {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
-spec(init(Args :: term()) ->
    {ok, State :: #seckill_center{}} | {ok, State :: #seckill_center{}, timeout() | hibernate} |
    {stop, Reason :: term()} | ignore).
init([]) ->
    erlang:process_flag(trap_exit, true),
    %% 加载购买日志
    SQL2 = <<"SELECT zone_id,goods_id,num,server,name,cost,tv_id,time FROM seckill_log order by time desc">>,
    BuyLogs =
        lists:foldl(
            fun([ZoneId, GoodsId, Num, Server, Name, _Cost, TvId, Time], Temp) ->
                OneLog = #seckill_buy_log{
                    server      = Server,
                    name        = Name,
                    goods_id    = GoodsId,
                    num         = Num,
                    tv_id       = TvId,
                    time        = Time
                },
                ZoneLogs = maps:get(ZoneId, Temp, []),
                maps:put(ZoneId, [OneLog|ZoneLogs], Temp)
            end, #{}, db:get_all(SQL2)
        ),
    State = #seckill_center{buy_log = BuyLogs},
    %% 判断是否有正在开启的活动
    case lib_custom_act_api:get_open_custom_act_infos(?CUSTOM_ACT_TYPE_SECKILL) of
        [_|_] = ActList ->
            ActData =
                [
                    {
                        SubType, #seckill_act{sub_type = SubType, begin_time = BeginTime, end_time = EndTime}
                    }
                    || #act_info{key = {_Type, SubType}, stime = BeginTime, etime = EndTime} <- ActList
                ],
            State1 = State#seckill_center{act_data = maps:from_list(ActData)},
            %% 准备下一场秒杀
            State2 = prepare_next_round_do(State1, re_calc),
            {ok, State2};
        _ ->
            %% 当前没有开活动
            %% 可能是本来就没开/也可能是停服后活动关闭了
            %% 什么都不处理，等其他活动消息驱动
            %% 保留日志
            {ok, State}
    end.

%%%===================================================================
%%% Logic 
%%%===================================================================
do_handle_call(_Request, _From, State) ->
    io:format("time:[~p], module:~p: do_handle_call unexpect message:~p~n", [erlang:localtime(), ?MODULE, _Request]),
    {reply, ok, State}.

%% 通用指定某个活动的某个分区的处理
do_handle_cast({common, SubType, ZoneId, Msg}, State) ->
    #seckill_center{
        act_data = ActData
    } = State,
    case maps:find(SubType, ActData) of
        {ok, #seckill_act{zone_data = ZoneData} = ActInfo} ->
            ZoneInfo = maps:get(ZoneId, ZoneData, #seckill_zone{zone_id = ZoneId}),
            case seckill_logic:handle(Msg, SubType, State, ZoneInfo) of
                {NewState, NewZoneInfo} when
                    is_record(NewState, seckill_center)
                    andalso is_record(NewZoneInfo, seckill_zone) ->
                    case NewZoneInfo of
                        ZoneInfo  ->
                            %% 未修改分区内活动数据
                            {noreply, NewState};
                        _ ->
                            NewActInfo  = ActInfo#seckill_act{zone_data = ZoneData#{ZoneId => NewZoneInfo}},
                            NewActData  = ActData#{SubType => NewActInfo},
                            {noreply, NewState#seckill_center{act_data = NewActData}}
                    end;
                _ ->
                    %% 忽略返回
                    {noreply, State}
            end;
        _ ->
            %% 活动未开
            {noreply, State}
    end;

do_handle_cast(_Request, State) ->
    io:format("time:[~p], module:~p: handle_cast unexpect message:~p~n", [erlang:localtime(), ?MODULE, _Request]),
    {noreply, State}.

%% 活动开启
do_handle_info({seckill_begin, Act}, State) ->
    %% 定制活动热更后就会走这里，如果结束时间变化了，需要更新
    #act_info{
        key = {_Type, SubType},
        stime = BeginTime,
        etime = ETime
    } = Act,
    #seckill_center{act_data = ActData} = State,
    case maps:find(SubType, ActData) of
        {ok, ActInfo} ->
            Now = utime:unixtime(),
            case ActInfo#seckill_act.end_time < Now of
                true  ->
                    %% 活动已经过期，那就新开活动
                    do_handle_info({seckill_begin_do, SubType, BeginTime, ETime}, State);
                false ->
                    %% 活动还未过期，更新时间？？
                    NewActInfo = ActInfo#seckill_act{begin_time = BeginTime, end_time = ETime},
                    {noreply, State#seckill_center{act_data = ActData#{SubType => NewActInfo}}}
            end;
        error ->
            %% 尚未开启活动，则直接开启
            do_handle_info({seckill_begin_do, SubType, BeginTime, ETime}, State)
    end;
%% 活动开启处理
do_handle_info({seckill_begin_do, SubType, BeginTime, ETime}, State) ->
    #seckill_center{act_data = ActData} = State,
    ActInfo = #seckill_act{sub_type = SubType, end_time = ETime, begin_time = BeginTime},
    State1 = State#seckill_center{act_data = ActData#{SubType => ActInfo}},
    %% 准备下一场秒杀
    State2 = prepare_next_round(State1, first_calc),
    {noreply, State2};

%% 0点消息对于正在开启中的活动，启动下一轮定时器
do_handle_info(seckill_zero_time, #seckill_center{act_data = ActData} = State)
    when map_size(ActData) == 0 ->
    %% 没有开活动。那就算了
    {noreply, State};
do_handle_info(seckill_zero_time, State)->
    %% 准备下一场秒杀
    State2 = prepare_next_round_do(State, first_calc),
    {noreply, State2};

%% 活动结束-一般是0点结束
do_handle_info({seckill_end, #act_info{key = {_Type, SubType}}}, #seckill_center{act_data = ActData} = State) ->
    %% 直接删掉数据即可
    NewActData = maps:remove(SubType, ActData),
    case map_size(NewActData) == 0 of
        true ->
            %% 活动全部结束，清理所有数据，仅保留日志
            util:cancel_timer(State#seckill_center.ref_notice),
            util:cancel_timer(State#seckill_center.ref_round),
            {noreply, #seckill_center{buy_log = State#seckill_center.buy_log}};
        false ->
            {noreply, State#seckill_center{act_data = NewActData}}
    end;

%% 预告通知
do_handle_info({seckill_notice, _Round}, State) ->
    %% 广播
    [
        seckill_local:seckill_notice(SubType) || SubType <- maps:keys(State#seckill_center.act_data)
    ],
    {noreply, State};

%% 场次开启
do_handle_info({seckill_round_begin, Round}, State)
    when Round =:= State#seckill_center.round ->
    %% 广播
    [
        seckill_local:seckill_round_begin(SubType, Round) || SubType <- maps:keys(State#seckill_center.act_data)
    ],
    %% 定时到场次结束
    EndTime = State#seckill_center.round_end,
    Now = utime:unixtime(),
    util:cancel_timer(State#seckill_center.ref_round),
    State1 = State#seckill_center{
        ref_round = erlang:send_after(max(0, EndTime - Now) * 1000, self(), {seckill_round_end, Round})
    },
    {noreply, State1};
do_handle_info({seckill_round_begin, Round}, State) ->
    ?ERR("receive wrong round_begin_msg:[receive:~p, now:~p]", [Round, State#seckill_center.round]),
    {noreply, State};

%% 场次结束
do_handle_info({seckill_round_end, _Round}, State) ->
    %% 日志可以等进程退出的时候再保存
    ResetActData = maps:fold(
        fun(Key, Value, Temp) ->
            Temp#{Key => Value#seckill_act{zone_data = #{}}}
        end, #{}, State#seckill_center.act_data
    ),
    State1 = State#seckill_center{act_data = ResetActData, ref_round = undefined, ref_notice = undefined},
    %% 准备下一场秒杀
    State2 = prepare_next_round_do(State1, re_calc),
    {noreply, State2};

%% 合区--使用主区的数据，将玩家购买数据从从区和过来，然后删掉从区数据
do_handle_info({recalc_zone, OldZoneId, NewZoneId}, State) ->
    #seckill_center{act_data = ActData, buy_log = BuyLogs} = State,
    %% 合并购买数据
    NewActData =
        maps:fold(
            fun(SubType, ActInfo, Temp) ->
                case maps:find(OldZoneId, ActInfo#seckill_act.zone_data) of
                    {ok, #seckill_zone{role_info = FromRoles}} ->
                        ActInfo1 = maps:remove(OldZoneId, ActInfo),
                        #seckill_zone{role_info = Roles}
                            = ToZoneData = maps:get(NewZoneId, Temp, #seckill_zone{zone_id = NewZoneId}),
                        NewToZoneData = ToZoneData#seckill_zone{role_info = maps:merge(FromRoles, Roles)},
                        NewActInfo = ActInfo1#seckill_act{zone_data = NewToZoneData},
                        Temp#{SubType => NewActInfo};
                    _ ->
                        Temp#{SubType => ActInfo}
                end
            end, #{}, ActData
        ),
    %% 删除从区购买日志
    NewBuyLogs = maps:remove(OldZoneId, BuyLogs),
    {noreply, State#seckill_center{act_data = NewActData, buy_log = NewBuyLogs}};

%% 秒杀记录
do_handle_info({ask_seckill_log, Sid, ZoneId}, State) ->
    #seckill_center{
        buy_log = ZoneData
    } = State,
    BuyLog = maps:get(ZoneId, ZoneData, []),
    LogsList =
        [
            {Server, Name, 0, GoodsId, Num, TvId}
            || #seckill_buy_log{
                    server      = Server,
                    name        = Name,
                    goods_id    = GoodsId,
                    num         = Num,
                    tv_id       = TvId
                } <- BuyLog
        ],
    {ok, Bin} = pt_331:write(33189, [LogsList]),
    lib_server_send:send_to_sid(Sid, Bin),
    {noreply, State};

do_handle_info(_Info, State) ->
    io:format("time:[~p], module:~p: handle_info unexpect message:~p~n", [erlang:localtime(), ?MODULE, _Info]),
    {noreply, State}.


%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
    State :: #seckill_center{}) ->
    {reply, Reply :: term(), NewState :: #seckill_center{}} |
    {reply, Reply :: term(), NewState :: #seckill_center{}, timeout() | hibernate} |
    {noreply, NewState :: #seckill_center{}} |
    {noreply, NewState :: #seckill_center{}, timeout() | hibernate} |
    {stop, Reason :: term(), Reply :: term(), NewState :: #seckill_center{}} |
    {stop, Reason :: term(), NewState :: #seckill_center{}}).
handle_call(Msg, From, State) ->
    try
        do_handle_call(Msg, From, State)
    catch _:_Error:Trace ->
        io:format("[~p]~nhandle_call Msg:~p~n Error:~p~n ~p~n", [erlang:localtime(), Msg, _Error, Trace]),
        {reply, error, State}
    end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_cast(Request :: term(), State :: #seckill_center{}) ->
    {noreply, NewState :: #seckill_center{}} |
    {noreply, NewState :: #seckill_center{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #seckill_center{}}).
handle_cast(Msg, State) ->
    try
        do_handle_cast(Msg, State)
    catch _:_Error:Trace ->
        io:format("[~p]~nhandle_cast Msg:~p~n Error:~p~n ~p~n", [erlang:localtime(), Msg, _Error, Trace]),
        {noreply, State}
    end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
-spec(handle_info(Info :: timeout() | term(), State :: #seckill_center{}) ->
    {noreply, NewState :: #seckill_center{}} |
    {noreply, NewState :: #seckill_center{}, timeout() | hibernate} |
    {stop, Reason :: term(), NewState :: #seckill_center{}}).
handle_info(Msg, State) ->
    try
        do_handle_info(Msg, State)
    catch _:_Error:Trace ->
        io:format("[~p]~nhandle_info Msg:~p~n Error:~p~n ~p~n", [erlang:localtime(), Msg, _Error, Trace]),
        {noreply, State}
    end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
    State :: #seckill_center{}) ->
    term()).
terminate(_Reason, State) ->
    batch_save_buy_log(State),
    ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #seckill_center{},
    Extra :: term()) ->
    {ok, NewState :: #seckill_center{}} | {error, Reason :: term()}).
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
%% 准备下一场秒杀
prepare_next_round(#seckill_center{act_data = ActData} = State, _Timing)
    when map_size(ActData) == 0->
    %% 如果没有活动就不启动
    State;
prepare_next_round(#seckill_center{ref_round = Ref} = State, _Timing)
    when is_reference(Ref)->
    State;
prepare_next_round(State, Timing) ->
    prepare_next_round_do(State, Timing).

prepare_next_round_do(State, Timing) ->
    Now = utime:unixtime(),
    case calc_round(Now, Timing) of
        {true, Round, BeginTime, EndTime, _Label} ->
            util:cancel_timer(State#seckill_center.ref_round),
            util:cancel_timer(State#seckill_center.ref_notice),
            RefRound  = erlang:send_after(max(0, BeginTime - Now) * 1000, self(), {seckill_round_begin, Round}),
            %%            RefNotice =
            %%                case Label > 1 andalso Round > 1 of
            %%                    true  ->
            %%                        erlang:send_after(max(0, BeginTime - Now - ?SECKILL_NOTICE_TIME) * 1000, self(), {seckill_notice, Round});
            %%                    false ->
            %%                        undefined
            %%                end,
            State#seckill_center{
                round         = Round
                , round_begin   = BeginTime
                , round_end     = EndTime
                , ref_round     = RefRound
            };
        {false, Round, BeginTime, EndTime, _Label} ->
            State#seckill_center{
                round         = Round
                , round_begin   = BeginTime
                , round_end     = EndTime
            };
        _ ->
            State
    end.
%% 计算下一场的场次和时间
calc_round(Now, Timing) ->
    Date = erlang:date(),
    RoundCfg = data_custom_act:get_seckill_round(),
    calc_round(RoundCfg, Timing, Now, Date).
calc_round([], _, _, _) ->
    false;
calc_round([{Round, {Hour, Min, Sec}, Duration, Label}|T], Timing, Now, Date) ->
    BeginTime = utime:unixtime({Date, {Hour, Min, Sec}}),
    EndTime   = BeginTime + Duration,
    if
        Now =< BeginTime ->
            {true, Round, BeginTime, EndTime, Label};
        Now < EndTime andalso Timing =:= first_calc->
            %% 判断的时候处于活动期间内，只对活动开启的第一场这么处理
            {true, Round, BeginTime, EndTime, Label};
        true ->
            case T of
                [] ->
                    %% 后面没有数据了，就用最后一场的数据
                    {false, Round, BeginTime, EndTime, Label};
                _  ->
                    calc_round(T, Timing, Now, Date)
            end
    end.

%% 批量保存购买记录
batch_save_buy_log(State) ->
    Sqls =
        maps:fold(
            fun(ZoneId, BuyLog, Temp) ->
                lists:foldl(
                    fun(Log, GoodsTemp) ->
                        #seckill_buy_log{
                            server      = Server,
                            name        = Name,
                            goods_id    = GoodsId,
                            num         = Num,
                            tv_id       = TvId,
                            time        = Time
                        } = Log,
                        [io_lib:format("(~p,~p,~p,~p,'~ts',~p,~p,~p)", [ZoneId, GoodsId, Num, Server, Name, 0, TvId, Time])|GoodsTemp]
                    end, Temp, BuyLog
                )
            end, [], State#seckill_center.buy_log
        ),
    case Sqls =/= [] of
        true  ->
            db:execute(<<"TRUNCATE seckill_log">>),
            SQL = io_lib:format(<<"INSERT INTO seckill_log(zone_id,goods_id,num,server,name,cost,tv_id,time) VALUES ~ts">>, [string:join(Sqls, ",")]),
            db:execute(SQL);
        false ->
            ignore
    end.