<?php
/**
 * Coupon Eloquent model
 *
 * @version 22/09/16
 * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */

namespace Mtc\Shop;

use Basket;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Mtc\Shop\Coupon\Code as CouponCode;

/**
 * Coupon Eloquent model.
 * Implements Shop coupon main functionality
 *
 * @version 22/09/16
 * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */
class Coupon extends Model
{
    /**
     * @var array The attributes that are mass assignable.
     */
    protected $fillable = [
        'code',
        'name',
        'redemptions',
        'type',
        'structure',
        'value',
        'exp_date',
        'min_price',
        'first_order',
        'code_type',
        'free_delivery',
        'non_sale_only'
    ];

    /**
     * @var array Available coupon types
     */
    public static $coupon_types = [
        'percent' => 'Percent off',
        'set' => 'Set Discount'
    ];

    /**
     * @var array Coupon structure
     */
    public static $coupon_structures = [
        'single' => 'Single code coupon',
        'multi' => 'Multi code coupon',
    ];

    /**
     * Scope - active()
     * Discards all hidden and deleted items
     *
     * @param \Illuminate\Database\Eloquent\Builder $query Query builder object
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('redemptions', '>', 0)
            ->where('from_date', '<=', date('Y-m-d'))
            ->where('exp_date', '>=', date('Y-m-d'));
    }

    /**
     * Scope - search()
     * Used for setting search params
     * @author Uldis Zvirbulis <uldis.zvirbulis@mtcmedia.co.uk>
     *
     * @param \Illuminate\Database\Eloquent\Builder $query Query builder object
     * @param array $value Array of search params
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeSearch($query, $value){
        if (!empty($value['name'])) {
            $query = $query->where('name', 'LIKE', '%'. trim($value['name']).'%');
        }
        if (!empty($value['code'])) {
            $query = $query->where('code', 'LIKE', '%'. trim($value['code']).'%');
        }
        if (!empty($value['usable'])) {
            $query = $query->whereRaw(" `redemptions` > 0 AND `exp_date` >= CURDATE() ");
        }
        return $query;
    }

    /**
     * Define the relationship to coupon conditions
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function conditions()
    {
        return $this->hasMany(Coupon\Condition::class, 'coupon_id');
    }

    /**
     * Define the relationship to coupon codes
     *
     * @author Uldis Zvirbulis <uldis.zvirbulis@mtcmedia.co.uk>
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function codes()
    {
        return $this->hasMany(CouponCode::class, 'coupon_id');
    }

    /**
     * Check if code already exists
     * @param string $code coupon code to check
     * @return bool whether this code already exists
     */
    public static function exists($code)
    {
        return self::where('code', $code)->count() > 0;
    }

    public static function validate($code, Basket $basket)
    {
        $coupon = self::active()
            ->where('code', $code)
            ->where('structure', 'single')
            ->first();
        /**
         * @var Coupon $coupon
         */
        // If there is no active coupon, look into coupon codes
        if (empty($coupon)) {
            $coupon_code = CouponCode::where('code', $code)
                ->where('order_id','0')
                ->first();
            if (!empty($coupon_code)) {
                $coupon = self::where('from_date', '<=', date('Y-m-d'))
                    ->where('exp_date', '>=', date('Y-m-d'))
                    ->find($coupon_code->coupon_id);
            }
        }
        if (!empty($coupon)) {
            $basket->coupon_name = $coupon->name;
            $basket->coupon_code = !empty($coupon_code) ? $coupon_code->code : $coupon->code;
            $basket->coupon_minspend = $coupon->min_price;
            $basket->coupon_first_order = $coupon->first_order;

            // Set delivery free delivery ir applicable
            if ($coupon->free_delivery == 1
                || ($basket->address['shipping']['country'] == 'GB' && $coupon->free_delivery == 2)
            ) {
                $basket->coupon_freedelivery = true;
                $basket->cost_delivery = 0;
                $basket->cost_total = $basket->cost_subtotal;
            }

            if ($coupon->type == 'set') {
                return $coupon->validateSetDiscount($basket);
            } elseif ($coupon->type == 'percent') {
                return $coupon->validatePercentDiscount($basket);
            }
        }
        $basket->coupon_code = '';
        return false;

    }

    /**
     * Check if set discount applies and set discount if it does
     * @param Basket $basket
     * @return bool set discount applies or not
     */
    private function validateSetDiscount(Basket $basket)
    {
        $discount_found = false;
        if (sizeof($basket->items) > 0) {
            // Check each item
            foreach ($basket->items as $item) {
                $found = $this->validateCouponRules($item, $basket);
                if ($found) {
                    $basket->coupon_amountof = $this->value;
                    $discount_found = true;
                }
            }
        }

        if ($discount_found && $basket->cost_subtotal >= $this->min_price) {
            $basket->coupon_deduct = $basket->coupon_amountof;
            if ($basket->coupon_amountof > $basket->cost_subtotal) {
                $basket->coupon_deduct = $basket->cost_subtotal;
            }
            return true;
        }
        // Valid discount wasn't found
        $basket->coupon_code = '';
        return false;
    }

    /**
     * Check if percent discount applies and set discount if it does
     * @param Basket $basket
     * @return bool percent discount applies or not
     */
    private function validatePercentDiscount(Basket $basket)
    {
        $basket->coupon_percentoff = $this->value;
        $discount_found = false;
        if (sizeof($basket->items) > 0) {
            // Check each item
            foreach ($basket->items as $k => $item) {
                $found = $this->validateCouponRules($item, $basket);
                if ($found) {
                    if ($basket->no_vat) {
                        $basket->coupon_deduct += round($item['afterdiscount_line_total_exvat'] * $basket->coupon_percentoff) / 100;
                    } else {
                        $basket->coupon_deduct += round($item['afterdiscount_line_total'] * $basket->coupon_percentoff) / 100;
                    }
                    $discount_found = true;
                    $basket->items[$k]['percentage_discount_applied'] = true;
                }
            }
        }
        if ($discount_found && $basket->cost_subtotal >= $this->min_price) {
            return true;
        }
        // Valid discount wasn't found
        $basket->coupon_code = '';
        $basket->coupon_deduct = 0;
        return false;
    }

    /**
     * Check all relevant conditions for basket item
     *
     * @param array $item basket item line
     * @param Basket $basket Current basket
     * @return boolean whether coupon conditions match item
     */
    private function validateCouponRules($item, Basket $basket)
    {
        if (!$this->checkItemConditions($item, $basket)) {
            return false;
        }

        if (!$this->checkCategoryConditions($item, $basket)) {
            return false;
        }

        if (!$this->checkBrandConditions($item, $basket)) {
            $this->coupon_error = 'This coupon does not apply to sale items';
            return false;
        }

        if ($this->non_sale_only == 1 && $item['original_price'] != $item['item_price']) {
            $this->coupon_error = 'This coupon does not apply to sale items';
            return false;
        }

        $email_is_set = !empty($basket->info['email']) ? $basket->info['email'] : false;
        if ($this->first_order == 1 && has_ordered($email_is_set, $this->member)) {
            $this->coupon_error = 'This coupon only applies to your first order';
            return false;
        }
        return true;
    }

    /**
     * Check Item restrictions for basket item
     * @param array $item basket item line
     * @param Basket $basket Current basket
     * @return boolean whether item conditions match basket item
     */
    private function checkItemConditions($item, Basket $basket)
    {
        $restricted_items = $this->conditions()
            ->where('type', 'item')
            ->pluck('value')
            ->toArray();
        if (!empty($restricted_items) && !in_array($item['item_id'], $restricted_items)) {
            $basket->coupon_error = 'This coupon does not apply to these items';
            return false;
        }
        // Empty restricted items == no item restrictions
        return true;
    }

    /**
     * Check Category restrictions for basket item
     * @param array $item basket item line
     * @param Basket $basket Current basket
     * @return boolean whether category conditions match basket item
     */
    private function checkCategoryConditions($item, Basket $basket)
    {
        $restricted_categories = $this->conditions()
            ->where('type', 'category')
            ->excluded(0)
            ->pluck('value')
            ->toArray();
        if (!empty($restricted_categories) && !array_intersect($restricted_categories, $item['categories'])) {
            $basket->coupon_error = 'This coupon does not apply to these categories';
            return false;
        }

        $excluded_categories = $this->conditions()
            ->where('type', 'category')
            ->excluded(1)
            ->pluck('value')
            ->toArray();

        if (!empty($excluded_categories) && array_intersect($excluded_categories, $item['categories'])) {
            $basket->coupon_error = 'This coupon does not apply to these categories';
            return false;
        }
        return true;
    }

    /**
     * Check Brand restrictions for basket item
     * @param array $item basket item line
     * @param Basket $basket Current basket
     * @return boolean whether brand conditions match basket item
     */
    private function checkBrandConditions($item, Basket $basket)
    {

        $restricted_brands = $this->conditions()
            ->where('type', 'brand')
            ->excluded(0)
            ->pluck('value')
            ->toArray();
        if (!empty($restricted_brands) && !in_array($item['brand'], $restricted_brands)) {
            $basket->coupon_error = 'This coupon does not apply to these brands';
            return false;
        }

        $excluded_brands = $this->conditions()
            ->where('type', 'brand')
            ->excluded(1)
            ->pluck('value')
            ->toArray();

        if (!empty($excluded_brands) && in_array($item['brand'], $excluded_brands)) {
            $basket->coupon_error = 'This coupon does not apply to these brands';
            return false;
        }
        return true;
    }

    /**
     * Generate a random coupon code
     * This code generates a coupon instance with a unique code
     *
     * @param array $config configuration of the coupon code
     * @return Coupon generated coupon code
     */
    public static function generateCoupon($config = [])
    {
        $prefix = $config['prefix'] ?? '';
        $code_length = $config['length'] ?? 20;
        $numbers_only = $config['numbers'] ?? false;

        // Coupon is created
        $chars = $numbers_only ? "0123456789" : "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

        $size = strlen ($chars);
        $generate_length = $code_length - strlen($prefix);

        // get a unique code
        $tries = 0;

        do {
            // break at 1000 failed tries to avoid infinite loop
            if (++$tries >= 1000) {
                break;
            }

            // set the base of the coupon code
            $coupon_code = $prefix;

            // Add $generate_length chars from the generate set
            for ($i = 0; $i < $generate_length; $i++) {
                $coupon_code .=  $chars[random_int(0, $size - 1)];
            }

            //check uniqueness
            $coupon_exists = self::exists($coupon_code);

            // Loop generation until we have a unique code
        } while ($coupon_exists);

        // By default the expiry is 1 year in future unless specified otherwise
        $expiry = Carbon::now()->addYear();
        if (!empty($config['exp_date'])) {
            $expiry = Carbon::createFromFormat('Y-m-d', $config['exp_date']);
        }

        return self::create([
            'code' => $coupon_code,
            'exp_date' => $expiry->format('Y-m-d'),
            'name' => $config['name'] ?? $coupon_code,
            'redemptions' => $config['redemptions'] ?? 1,
            'type' => $config['type'] ?? 'set',
            'structure' => $config['type'] ?? 'single',
            'value' => $config['value'] ?? 0,
            'min_price' => $config['min_price'] ?? 0,
            'first_order' => $config['first_order'] ?? 0,
            'code_type' => $config['code_type'] ?? '',
            'free_delivery' => $config['free_delivery'] ?? 0,
            'non_sale_only' => $config['non_sale_only'] ?? 0,
        ]);

    }

}
