<?php

namespace Yajny\Helpers\LoyaltyProgram;

use Yajny\Exceptions\LoyaltyProgramInvalidStartDateException;
use Yajny\Helpers\LoyaltyProgram\Requests\UserBonusInsertRequest;
use Yajny\Helpers\LoyaltyProgram\Requests\UserBonusUpdateRequest;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Yajny\EventsYj\Events\BonusNew;
use Yajny\EventsYj\Events\BonusUpdated;
use Yajny\EventsYj\Events\LoyaltyAchievement;
use Yajny\Member\Models\LoyaltyBonus;
use Yajny\Member\Models\LoyaltyProgram;
use Yajny\Member\Models\Member;
use Yajny\Member\Models\UserBonus;
use Yajny\Member\Models\UserBonusSale;
use Yajny\Member\Models\UserSales;
use Yajny\Models\Store;

class LoyaltyProgramCalculator
{
    protected $date_from;
    protected $date_to;
    protected $timezone;
    protected $kickoffDate;
    protected $excludedStoresSlugs;
    protected $is_silent;
    public $loyalty_programs;

    public function __construct($date_from = null)
    {
        $this->kickoffDate = "05-02-2022";
        $this->timezone = 'Asia/Riyadh';
        $this->is_silent = false;
        $this->loyalty_programs = LoyaltyProgram::orderBy('rank', 'DESC')->get();
        $this->excludedStoresSlugs = [
            'amazon'
        ];

        if ($date_from) {
            $this->setDates($date_from);
        } else {
            $last_friday_time_stamp = strtotime("last friday");
            $last_friday = Carbon::createFromTimeStamp($last_friday_time_stamp)
                ->timezone($this->timezone)->endOfDay()->timezone('UTC')->toDateTimeString();
            $saturday_before = Carbon::createFromTimeStamp($last_friday_time_stamp)->subDays(6)
                ->timezone($this->timezone)->startOfDay()->timezone('UTC')->toDateTimeString();

            $this->date_from = $saturday_before;
            $this->date_to = $last_friday;
        }
    }

    public function mute()
    {
        $this->is_silent = true;
    }

    public function isDateAfterKickoff()
    {
        $kickoffDate = Carbon::createFromFormat('d-m-Y', $this->kickoffDate, $this->timezone)->startOfDay();
        $fromDate = Carbon::parse($this->date_from)->timezone($this->timezone);
        return $kickoffDate->lte($fromDate);
    }

    public function setDateFrom($date_from)
    {
        $this->setDates($date_from);
    }

    protected function setDates($date_from)
    {
        $start_date = Carbon::parse($date_from, $this->timezone);
        if (!$start_date->isSaturday())
            throw new LoyaltyProgramInvalidStartDateException();

        $cloned_start_date = $start_date->copy();

        $this->date_from = $start_date->startOfDay()->timezone('UTC')->toDateTimeString();
        $this->date_to = $cloned_start_date->addDays(6)->endOfDay()->timezone('UTC')->toDateTimeString();
    }

    public function calculateLoyaltyBonuses(array $user_ids = [])
    {
        if (!$this->isDateAfterKickoff())
            return;

        $user_sales_records = $this->getEligibleUserSales($user_ids);
        foreach ($user_sales_records as $user_id => $user_sales) {
            list($selected_program, $eligible_sale_ids, $confirmed_sales_count) = $this->calculateBonus($user_sales);
            $old_bonus = $this->checkOldLoyaltyBonus($user_id);
            if (count($eligible_sale_ids) >= $this->loyalty_programs->min('min_sale_count') && $selected_program) {
                if ($old_bonus) {
                    if ($old_bonus->loyaltyProgram->rank > $selected_program->rank)
                        $this->declineBonus($old_bonus);
                    else {
                        $updateRequest = new UserBonusUpdateRequest($old_bonus, $selected_program, $confirmed_sales_count, $eligible_sale_ids);
                        $this->updateOldBonus($updateRequest);
                        if ($old_bonus->loyaltyProgram->rank != $selected_program->rank) {
                            $user = Member::find($user_id);
                            event(new LoyaltyAchievement($user, $selected_program, $this->is_silent));
                        }
                    }
                } else {
                    $insertRequest = new UserBonusInsertRequest($user_id, $selected_program, $confirmed_sales_count, $eligible_sale_ids);
                    $this->addNewBonus($insertRequest);
                    $user = Member::find($user_id);
                    event(new LoyaltyAchievement($user, $selected_program, $this->is_silent));
                }
            } else {
                if ($old_bonus)
                    $this->declineBonus($old_bonus);
            }
        }
    }

    public function getEligibleUserSales(array $user_ids = [])
    {
        $excludedStoresIDs = Store::whereIn('slug', $this->excludedStoresSlugs)->get()->pluck('id')->toArray();

        $subQuery = UserSales::whereBetween('user_sales.transaction_time', [$this->date_from, $this->date_to])
            ->where('user_sales.order_amount', '>=', $this->loyalty_programs->min('min_sale_amount'))
            ->whereIn('user_sales.status', ['pending', 'confirmed'])
            ->join('users', 'users.user_id', '=', 'user_sales.user_id')
            ->where('users.is_merchant', 0)
            ->whereNotIn('user_sales.store_id', $excludedStoresIDs)
            ->groupBy('user_id')
            ->groupBy(DB::raw('Date(user_sales.transaction_time)'))
            ->havingRaw(DB::raw('MAX(user_sales.order_amount)'))
            ->select(DB::raw('Date(user_sales.transaction_time) as grouped_date'), DB::raw('MAX(user_sales.order_amount) as max'), 'user_sales.user_id');

        if (!empty($user_ids)) {
            $subQuery->whereIn('user_sales.user_id', $user_ids);
        }

        $query = UserSales::joinSub($subQuery, 'subQuery', function ($join) {
            $join->on(DB::raw('Date(user_sales.transaction_time)'), '=', 'subQuery.grouped_date');
            $join->on('user_sales.order_amount', '=', 'subQuery.max');
            $join->on('user_sales.user_id', '=', 'subQuery.user_id');
        })
            ->groupBy('user_id')
            ->groupBy(DB::raw('Date(user_sales.transaction_time)'))
            ->havingRaw(DB::raw('MAX(user_sales.order_amount)'))
            ->orderBy('user_sales.user_id', 'ASC')
            ->orderBy('user_sales.order_amount', 'DESC')
            ->select('user_sales.*');

        return $query->get()->groupBy('user_id');
    }

    public function calculateBonus($user_sales)
    {
        foreach ($this->loyalty_programs as $loyalty_program) {
            $program_sales_count = 0;
            $confirmed_sales_count = 0;
            $eligible_sale_ids = [];
            foreach ($user_sales as $user_sale) {
                if ($user_sale->order_amount >= $loyalty_program->min_sale_amount) {
                    $program_sales_count++;
                    $eligible_sale_ids[] = $user_sale->id;

                    if ($user_sale->status == 'confirmed')
                        $confirmed_sales_count++;
                }
            }

            if ($program_sales_count >= $loyalty_program->min_sale_count)
                return [$loyalty_program, $eligible_sale_ids, $confirmed_sales_count];
        }

        return [null, [], 0];
    }

    public function checkOldLoyaltyBonus($user_id)
    {
        return UserBonus::join('loyalty_bonuses', 'user_bonus.id', '=', 'loyalty_bonuses.user_bonus_id')
            ->where('user_bonus.user_id', $user_id)
            ->where('user_bonus.bonus_code', 'loyalty_program')
            ->where('loyalty_bonuses.date_from', $this->date_from)
            ->where('loyalty_bonuses.date_to', $this->date_to)
            ->select(['user_bonus.*', 'loyalty_bonuses.date_from', 'loyalty_bonuses.date_to'])
            ->first();
    }

    protected function syncUserBonusSales($bonus_id, $accepted_user_sale_ids)
    {
        $data = [];
        array_walk($accepted_user_sale_ids, function ($value) use ($bonus_id, &$data) {
            $local_data['user_sale_id'] = $value;
            $local_data['user_bonus_id'] = $bonus_id;
            $data[] = $local_data;
        });
        UserBonusSale::where('user_bonus_id', $bonus_id)->delete();
        UserBonusSale::insert($data);
    }

    protected function updateLoyaltyBonus($bonus_id, $loyalty_program_id)
    {
        $loyalty_bonus = LoyaltyBonus::firstOrNew(['user_bonus_id' => $bonus_id]);
        $loyalty_bonus->loyalty_program_id = $loyalty_program_id;
        $loyalty_bonus->date_from = $this->date_from;
        $loyalty_bonus->date_to = $this->date_to;
        $loyalty_bonus->save();
    }

    public function addNewBonus(UserBonusInsertRequest $request)
    {
        $new_bonus = new UserBonus;
        $new_bonus->user_id = $request->user_id;
        $new_bonus->bonus_code = 'loyalty_program';
        $new_bonus->amount = (double)$request->selected_program->bonus_amount;
        $new_bonus->awarded_on = now()->toDateString();
        $new_bonus->expires_on = null;
        $new_bonus->status = ($request->confirmed_sales_count >= $request->selected_program->min_sale_count) ? 'confirmed' : 'pending';
        $new_bonus->save();

        $this->syncUserBonusSales($new_bonus->id, $request->eligible_sale_ids);
        $this->updateLoyaltyBonus($new_bonus->id, $request->selected_program->id);
        event(new BonusNew($new_bonus->user, $new_bonus, $this->is_silent));

        return $new_bonus;
    }

    public function updateOldBonus(UserBonusUpdateRequest $request)
    {
        $old_bonus = $request->old_bonus;

        $old_bonus->update([
            'bonus_code' => 'loyalty_program',
            'amount' => (double)$request->selected_program->bonus_amount,
            'status' => ($request->confirmed_sales_count >= $request->selected_program->min_sale_count) ? 'confirmed' : 'pending'
        ]);

        $this->syncUserBonusSales($old_bonus->id, $request->eligible_sale_ids);
        $this->updateLoyaltyBonus($old_bonus->id, $request->selected_program->id);
    }

    public function declineBonus(UserBonus $bonus)
    {
        $bonus->update(['status' => 'declined']);
        event(new BonusUpdated($bonus->user, $bonus, $this->is_silent));
    }
}
