<?php

use App\Events\BasketItemsLoadedEvent;
use App\Events\BasketLoadedEvent;
use App\Events\BasketRemoveItemEvent;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Mtc\Core\Currency;
use Mtc\Core\Models\Country;
use Mtc\Core\Models\CountryState;
use Mtc\Core\PostcodeFactory;
use Mtc\Modules\Members\Classes\Auth;
use Mtc\Modules\Members\Models\Member;
use Mtc\Plugins\BasketBuilder\Classes\BasketBuilder;
use Mtc\Plugins\NHS\Classes\PrescriptionItem;
use Mtc\Shop\Basket\Address;
use Mtc\Shop\Basket\Info as BasketInfo;
use Mtc\Shop\Basket\Protx;
use Mtc\Shop\Coupon;
use Mtc\Shop\DeliveryZone;
use Mtc\Shop\Events\BasketAlternateDeliveryCheckEvent;
use Mtc\Shop\Events\BasketGetItemsEvent;
use Mtc\Shop\Ingredient;
use Mtc\Shop\Item\Size;
use MtcPharmacy\Bundles\Classes\Bundle;
use MtcPharmacy\Subscriptions\Classes\SubscriptionPreference;

/**
 * Basket.
 *
 * @author mtc
 * @copyright 2013 mtc. http://www.mtcmedia.co.uk/
 *
 * @version 2013
 */
class Basket
{
    public $flag_type;
    public $require_id_check;
    public $currency;
    public $cost_subtotal_beforediscounts;
    public $verify_email;
    public $verify_phone;

    public static $duration = 900; //seconds
    public $id;
    public $subscription_id;
    public $member;
    public $items;
    public $num_items = 0;
    public $item_stock;
    public $instock = true;
    public $stock_error;
    public $delivery_error;
    public $limit_error;
    public $general_error;
    public $incompatibility_error;
    public $sale_item;
    public $additionals = false;

    //Delivery Details
    public $delivery_heavy = 0;
    public $delivery_options;
    public $delivery_selected;
    public $delivery_instructions;

    //Coupon Options
    public $coupon_code; //The code of the coupon code
    public $coupon_name; //The name of the coupon code
    public $coupon_freedelivery; //If there is free delivery with coupon code
    public $coupon_amountof; //Set Amount Off
    public $coupon_percentoff; //Percentage to be taken off
    public $coupon_deduct; //Amount that will be deducted from sub total;
    public $coupon_error;
    public $coupon_message;
    public $coupon_first_order;

    //Costs
    public $cost_subtotal = 0;
    public $cost_subtotal_exvat = 0;
    public $cost_delivery;
    public $cost_vat = 0;
    public $cost_total;
    public $cost_total_exvat;
    public $vat_deductable_amount = 0;
    public $no_vat = false;
    //Order Related Info
    public $info;
    public $weight = 0;
    /**
     * @var array list of basket addresses - billing & shipping
     */
    public $address;
    public $protx;
    public $expiry_time;
    public $ref;
    public $keywords;
    public $basket_html;
    public $zone;
    /**
     * @var int $allow_alternate_delivery whether to allow user to change shipping address
     */
    public $allow_alternate_delivery = 0;
    public bool $has_physical_items = true;

    /**
     * Basket type: Shop/Trade etc.
     *
     * @var string
     */
    public $type;

    /**
     * @var int $billing_address_used whether billing or shipping address is used in basket
     */
    public $billing_address_used = 1;

    //Discounts
    public $discounts_found = [];
    public $discounts = [];
    public $total_discount;
    public $singleitems = [];

    public $contains_doctor_pharmacy_items = false;

    /**
     * Currency Rates
     * @var array
     */
    public $c_rates = [];

    const NEXT_DAY_DELIVERY_FROM = 200;

    /**
     * @param $id
     *
     * @return
     */
    public function __construct($id = null)
    {
        //If existing basket use it, else create it.
        if (!empty($id)) {
            $basket = \Mtc\Shop\Basket::query()->find($id);
            if (empty($basket)) {
                return;
            }
            $this->id = $id;
            return;
        }
        if ($member = Auth::getLoggedInMember()) {
            if (!empty($member->basket_id)) {
                $_SESSION['basket_id'] = $member->basket_id;
                $this->id = $member->basket_id;
                return;
            }
        }
        if (!empty($_SESSION['basket_id'])) {
            $basket = \Mtc\Shop\Basket::query()->find($_SESSION['basket_id']);
            if (!empty($basket)) {
                $this->id = $_SESSION['basket_id'];
            }

            // check if an order has been paid against this basket and clear the session
            if (UNSET_PAID_BASKETS) {
                $paidOrderExists = \Mtc\Shop\Order::query()
                    ->where('basket_id', $_SESSION['basket_id'])
                    ->where('paid', 1)
                    ->exists();

                if ($paidOrderExists) {
                    unset($this->id);
                    unset($_SESSION['basket_id']);
                    if (MEMBERS_BASKET === true && !empty($_SESSION['member_id'])) {
                        Member::unsetMemberBasket($_SESSION['member_id']);
                    }
                }
            }
        }
    }

    /**
     * Unset the session as the data has been updated in database, so we need to re-load it all
     */
    public function resetSession()
    {
        if (isset($_SESSION)) {
            unset($_SESSION['basket']);
        }
    }

    /**
     * When class attributes get updated, the session needs to be updated as well
     */
    public function updateSession()
    {
        $_SESSION['basket'] = (array)$this;
        $_SESSION['basket_id'] = $this->id;
    }

    /**
     * Basket::Go_Basket().
     *
     * @return
     */
    public function Go_Basket($skip_zones_and_limits = false)
    {

        $this->Get_Basics();

        //Defaults to stop js errors
        $this->info['dob_date'] = '';
        $this->info['dob_month'] = '';
        $this->info['dob_year'] = '';
        $this->info['email'] = '';
        $this->info['contact_no'] = '';
        $this->info['phone_prefix'] = '+44';
        $this->address['billing'] = [];

        $this->items = [];
        $this->Get_Items();
        $this->has_physical_items = $this->getHasPhysicalItems();
        $this->Get_CustomerInfo();
        $this->Get_CustomerAddress();
        $this->Get_Delivery_Choices();
        $this->getCurrencyRates();
        $this->Calculate_Delivery_Cost();

        $this->Discounts(); // We apply automatic discounts before coupons

        $this->Validate_Coupon($this->coupon_code);
        $this->generateCouponMessage();

        $this->vat_reduction();
        $this->Total_Costs();

        $this->Stock_Check();

        if (!$skip_zones_and_limits) {
            $this->validateRestrictedZones();
            $this->validateIngredientLimits();
            $this->validateIncompatibleCategories();
        }

        if ($this->coupon_freedelivery) {
            $this->delivery_options[$this->delivery_selected]['name'] = 'Free Delivery Coupon';
        }

        //Add DOB from member details
        if (!empty($this->member)) {

            $member = Member::find((int)$this->member);

            if (!empty($member)) {

                $d = !empty($member->dob_date) ? $member->dob_date : '';
                $m = !empty($member->dob_month) ? $member->dob_month : '';
                $y = !empty($member->dob_year) ? $member->dob_year : '';

                if (empty($this->info['dob_date'])) {
                    $this->info['dob_date'] = str_pad($d, 2, 0, STR_PAD_LEFT);
                }

                if (empty($this->info['dob_month'])) {
                    $this->info['dob_month'] = str_pad($m, 2, 0, STR_PAD_LEFT);
                }

                if (empty($this->info['dob_year'])) {
                    $this->info['dob_year'] = (string)$y;
                }

                if (empty($this->info['email'])) {
                    $this->info['email'] = $member->email;
                }

                if (empty($this->info['contact_no'])) {
                    $this->info['contact_no'] = $member->contact_no;
                }

            }
        }


        // This is useful as GB will always be the initial country for the basket
        if (($this->address['billing']['country'] ?? 'GB') === 'GB') {
            $this->info['phone_prefix'] = '+44';
        } else {
            // We simply find the country that is the current billing address
            // And assign its dial code to the prefix field
            $country = Country::query()->where('code', $this->address['billing']['country'])->first();
            if ($country) {
                $this->info['phone_prefix'] = '+' . $country->dial_code;
            }
        }

        if (SHIPPING_ADDRESS === true && $this->has_physical_items) {
            $this->allow_alternate_delivery = 1;
            // Check if there any events that don't want the shipping address to be present
            $preventions = Event::dispatch(new BasketAlternateDeliveryCheckEvent($this));
            foreach ($preventions as $prevention) {
                if (!empty($prevention)) {
                    $this->allow_alternate_delivery = 0;
                }
            }
        }

        $this->info['multisite__site_id'] = SITE_ID;

        // When basket has loaded all it's elements, put it in session.
        $this->updateSession();

        $this->contains_doctor_pharmacy_items = $this->containsItemsNeedingReview();
        Event::dispatch(BasketLoadedEvent::class, new BasketLoadedEvent($this));
    }

    /**
     * Basket::Get_Basics().
     *
     * @return void
     */
    public function Get_Basics(): void
    {
        $basket = \Mtc\Shop\Basket::query()
            ->find($this->id);
        if (empty($basket)) {
            return;
        }
        $data = $basket->toArray();
        foreach ($data as $key => $value) {
            if ($key === 'basket_expiry_time') {
                $this->expiry_time = $value;
                continue;
            }
            if ($key === 'coupon') {
                $this->coupon_code = $value;
                continue;
            }
            $this->$key = $value;
        }
    }

    /*
    Creates a new basket
    */

    /**
     * Basket::Create_Basket().
     *
     * @return
     */
    public function Create_Basket()
    {
        $basket = Mtc\Shop\Basket::create([
            'date' => date('Y-m-d H:i:s'),
            'member' => empty($_SESSION['member_id']) ? 0 : $_SESSION['member_id'],
            'ref' => !empty($_COOKIE['ref']) ? $_COOKIE['ref'] : '',
            'keywords' => !empty($_COOKIE['keywords']) ? $_COOKIE['keywords'] : '',

        ]);
        $this->id = $basket->id;

        // If member creates basket, set default values for basket
        if (!empty($_SESSION['member_id'])) {
            $member = Auth::getLoggedInMember();
            $this->setDefaults($member);
        } else {
            /*
             * Initialize address
             * Basket requires at least one address with country upon creation
             */
            $default = [
                'country' => 'GB'
            ];
            $this->setCustomerAddress('billing', $default);
            // Set users info fields as empty array as no data available
            $this->setCustomerInfo([]);
        }

        $_SESSION['basket_id'] = $this->id;
    }

    /**
     * Store basket default info based on member data
     * Used when a logged in member adds item to an empty basket
     * Used when member signs in to a basket without details
     *
     * @param Member $member current logged in user
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function setDefaults(Member $member)
    {
        // Set billing address from member details

        $address = $member->addressBilling->toArray();
        unset($address['id']);
        unset($address['member_id']);
        if (!empty($member->gender)) {
            $address['gender'] = $member->gender;
        }

        $this->setCustomerAddress('billing', $address);

        // Set users info fields
        $this->setCustomerInfo([
            'email' => $member->email,
            'phone_prefix' => $member->phone_prefix,
            'contact_no' => $member->contact_no,
            'mobile_no' => $member->mobile_no,
        ]);

        // If by any chance user has shipping details, use them
        if (!empty($member->address['shipping'])) {
            $address = $member->address['shipping'];
            unset($address['id']);
            unset($address['member_id']);
            if (!empty($member->gender)) {
                $address['gender'] = $member->gender;
            }
            $this->setCustomerAddress('shipping', $address);
        }
    }

    /**
     * Adds new Item to basket
     *
     * @param string[] $params params for adding item into basket
     * @return int ID of the newly added (updated) item
     */
    public function Add_Item($params, $merge_quantities = true)
    {
        if (empty($_SESSION['basket_id'])) {
            $this->Create_Basket();
        }
        $basketItem = \Mtc\Shop\Basket\Item::query()
            ->where('basket_id', $this->id)
            ->where('item_id', $params['id'])
            ->where('size', $params['size'])
            ->first();

        if (!$merge_quantities || empty($basketItem)) {
            if ($params['quantity'] > 0) {
                $params['quantity'] = $this->enforceMaxQuantitySize($params['quantity']);

                $db_params = [
                    'basket_id' => $this->id,
                    'item_id' => $params['id'],
                    'quantity' => $params['quantity'],
                    'recommended_quantity' => $params['recommended_quantity'] ?? 1,
                    'size' => (string)$params['size'],
                    'sizeid' => (int)$params['size_id'],
                    'PLU' => (string)$params['PLU'],
                    'assessment_id' => !empty($params['assessment_id']) ? (int)$params['assessment_id'] : 0,
                ];

                if (!empty($params['nhs_prescription'])) {
                    $db_params['nhs_prescription'] = $params['nhs_prescription'];
                    $db_params['refill_date'] = $params['refill_date'];
                }

                $basketItem = \Mtc\Shop\Basket\Item::query()
                    ->create($db_params);
            }
        } else {
            $new_quantity = $this->enforceMaxQuantitySize((int)$basketItem->quantity + (int)$params['quantity']);

            \Mtc\Shop\Basket\Item::query()
                ->where('id', $basketItem->id)
                ->update([
                    'quantity' => $new_quantity
                ]);
        }
        HooksAdapter::do_action(__CLASS__ . '/' . __FUNCTION__, $basketItem->toArray(), $_REQUEST);

        $this->resetSession();
        // Return the basket_item id
        return $basketItem->id;
    }

    /**
     * Process request when user has added item to basket by using quick-buy
     *
     * @param Basket $basket current basket
     * @param Item $item item that is added to basket
     * @param Member|null $member instance of current member or null if MEMBERS disabled
     * @param array $request $_REQUEST values
     * @return array response array containing status and url
     * @author Isaac Montero
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public static function quickBuyTrigger(Basket $basket, Item $item, $member, $request)
    {
        $response = [];
        if (isset($request['quick_buy'])) {
            if (!empty($member->id)) {
                //Check for stock enough quantity
                if (!empty($basket->stock_error)) {
                    $response['status'] = 'ok';
                    $response['url'] = '/shop/checkout/basket.php';
                }

                // QUICK BUY - auto-populate require basket fields from stored member information
                $basket->setCustomerInfo([
                    'email' => $member->email,
                    'contact_no' => $member->contact_no,
                    'mobile_no' => $member->mobile_no,
                ]);

                $basket->address = $member->address;
                if (empty($member->address['shipping'])) {
                    $basket->address['shipping'] = $member->address['billing'];
                }

                foreach ($basket->address as $type => $address) {
                    $basket->address[$type]['id'] = '';
                }
                $basket->Add_CustomerAddress();

                if (empty($basket->delivery_selected)) {
                    $basket->delivery_selected = 0;
                    $basket->Set_Delivery_Cost();
                }

                // basket_overview runs $basket->validate() so any errors will be picked up there
                $response['status'] = 'ok';
                $response['url'] = '/shop/checkout/basket_overview.php';

            } else {
                $response['status'] = 'ok';
                $response['url'] = route('members-login', [
                    'redirect' => urlencode($item->url)
                ]);
            }
        }
        $basket->resetSession();
        return $response;
    }

    /**
     * Basket::Get_Items()
     *
     * @return array
     */
    public function Get_Items(): array
    {
        if (empty($this->id)) {
            return [];
        }
        $basketItems = \Mtc\Shop\Basket\Item::query()
            ->where('basket_id', $this->id)
            ->get();

        foreach ($basketItems as $basketItem) {
            $data = $basketItem->toArray();
            //Get Items name
            $item = new Item();
            $item->Get_Item($data['item_id']);
            $data['item_image'] = $item->image_basket;
            $data['item_image_alt'] = $item->image_basket_alt ?? null;
            $data['item_url'] = $item->url;
            $data['item_name'] = $item->name;
            $data['preorder'] = $item->preorder;
            $data['item_price'] = $item->price;
            $data['original_price'] = $item->price;
            $data['item_price_exvat'] = $item->price_exvat;

            if (isset($item->custom['shipper_weight'])) {
                $this->weight += $item->custom['shipper_weight'] * $data['quantity'];
            }

            if ($item->sale_price > 0) {
                $data['item_price'] = $item->sale_price;
                $data['item_price_exvat'] = $item->sale_price_exvat;
            }
            // if item size prices are turned on
            if (ITEMS_SIZES_PRICES) {
                $size = Size::query()
                    ->find($data['sizeid']);

                if (!empty($size)) {
                    $sdata = $size->toArray();
                    // get size prices from database
                    // if size prices are set, overwrite main item prices
                    if ($sdata['price'] > 0) {
                        $data['item_price'] = $sdata['price'];
                        $data['original_price'] = $sdata['price'];
                        $data['item_price_exvat'] = $sdata['price_exvat'];
                    }

                    if ($sdata['sale_price'] > 0) {
                        $data['item_price'] = $sdata['sale_price'];
                        $data['item_price_exvat'] = $sdata['sale_price_exvat'];
                    }
                }
            }

            //This is the price after multi buy discounts have been applied.
            $data['afterdiscount_item_price'] = $data['item_price'];
            $data['afterdiscount_item_price_exvat'] = $data['item_price_exvat'];

            //Currency
            if (!empty($_SESSION['currency']['currency'])) {
                $conversion_rate = $this->c_rates[$_SESSION['currency']['currency']] ?? 1;
                $data['price_converted'] = $data['item_price'] * $conversion_rate;
            }

            $data['item_heavy'] = $item->heavy;
            $data['vat_deductable'] = $item->vat_deductable;
            $data['vat_rate'] = $item->vat_rate;
            if ($item->heavy) {
                $this->delivery_heavy = 1;
            }

            if ($item->vat_deductable) {
                $this->vat_deductable_amount += ($data['item_price'] - $data['item_price_exvat']);
            }

            if ($item->isHidden()) {
                $data['quantity_lock'] = $data['disable_url'] = true;
            }

            if ($item->basket_quantity_locked || $data['has_requested_subscription']) {
                $data['quantity_lock'] = true;
            }

            // these are changed if a percentage discount is applied.
            $data['price_paid'] = $data['item_price'];
            $data['price_paid_exvat'] = $data['item_price_exvat'];

            if ($data['prescription_item_id']) {
                $prescription_item = PrescriptionItem::find($data['prescription_item_id']);
                $data['nhs_prescription_item'] = $prescription_item->toArray();
                $data['item_name'] = 'NHS Prescription Item // ' . $prescription_item->name;
                $data['item_price'] = $prescription_item->get_price();
                $data['item_price_exvat'] = $data['item_price'];
                $data['original_price'] = $prescription_item->get_price();
                $data['price_paid'] = $prescription_item->get_payable_price();
                $data['price_paid_exvat'] = $data['price_paid'];
                $data['quantity'] = $prescription_item->quantity;
            }

            if ($data['bundle_id']) {
                $bundle = Bundle::find($data['bundle_id']);
                $data['item_name'] .= " ({$bundle->type->name} bundle)";
                $data['item_url'] = "{$bundle->type->url}?bundle_id={$bundle->id}";
            }

            $data['line_total'] = $data['item_price'] * $data['quantity'];
            $data['original_total'] = $data['original_price'] * $data['quantity'];
            $data['line_total_exvat'] = $data['item_price_exvat'] * $data['quantity'];

            //After discount line totals
            $data['afterdiscount_line_total'] = $data['afterdiscount_item_price'] * $data['quantity'];
            $data['afterdiscount_line_total_exvat'] = $data['afterdiscount_item_price_exvat'] * $data['quantity'];

            $data['categories'] = [];
            if (sizeof($item->categories) > 0) {
                foreach ($item->categories as $cat) {
                    $data['categories'][] = $cat['cat_id'];
                }
            }
            $data['brand'] = $item->brand;


            $this->cost_subtotal += $data['line_total'];
            $this->cost_subtotal_exvat += ($data['item_price_exvat'] * $data['quantity']);
            $this->cost_vat += ($data['item_price'] - $data['item_price_exvat']) * $data['quantity'];

            // Load Item additions and add additional costs if applicable
            $additions = Event::dispatch(new BasketGetItemsEvent($data, $this));
            if (!empty($additions)) {
                $data['additional'] = [];
                // Since one functionality can have multiple additionals merge them in one array
                foreach ($additions as $addition_array) {
                    if (is_array($addition_array)) {
                        $data['additional'] = array_merge($data['additional'], $addition_array);
                    }
                }
                // Loop through additionals to check for price changes for item
                foreach ($data['additional'] as $addition) {
                    if (!empty($addition['item_price'])) {
                        $data['item_price'] += $addition['item_price'];
                    }
                    if (!empty($addition['item_price_exvat'])) {
                        $data['item_price_exvat'] += $addition['item_price_exvat'];
                    }
                    if (!empty($addition['line_total'])) {
                        $data['line_total'] += $addition['line_total'];
                    }
                    if (!empty($addition['line_total_exvat'])) {
                        $data['line_total_exvat'] += $addition['line_total_exvat'];
                    }
                    if (!empty($addition['cost_subtotal'])) {
                        $this->cost_subtotal += $addition['cost_subtotal'];
                    }
                    if (!empty($addition['cost_subtotal_exvat'])) {
                        $this->cost_subtotal_exvat += $addition['cost_subtotal_exvat'];
                    }
                    if (!empty($addition['quantity_lock'])) {
                        $data['quantity_lock'] = true;
                    }
                }
            }
            $data['product_type'] = $item->product_type;
            $data['can_remove'] = true;

            $data['manage_subscription_button'] = ['url' => '', 'label' => ''];


            $sub_config = SubscriptionPreference::init($data['item_id'], $data['sizeid']);
            if ($sub_config) {
                if ($sub_config->is_required) {
                    $data['manage_subscription_button']['label'] = 'Subscription only.';
                } elseif ($sub_config->is_allowed) {
                    $data['manage_subscription_button']['url'] = route('shop-select-subscription', [$data['item_id'], $data['sizeid'],], false);
                    $data['manage_subscription_button']['label'] = 'Add subscription...';
                }
            }

            if ($data['has_requested_subscription']) {
                $data['manage_subscription_button']['label'] = 'Subscription included.';
            }

            if ($item->is_groupbuy_container) {
                $included_shop_items = $item->eloquent()->groupbuy_bundletype->getShopItems();
                foreach ($included_shop_items as $included_shop_item) {
                    $data['included_groupbuy_items'][] = [
                        'id' => $included_shop_item->id,
                        'url' => $included_shop_item->url,
                        'name' => $included_shop_item->name,
                    ];
                }
            }
            $this->num_items += $data['quantity'];

            //Add Item to the list
            $this->items[] = $data;
        }

        //modify basket items
        HooksAdapter::do_action(__CLASS__ . '/' . __FUNCTION__, $this);
        Event::dispatch(BasketItemsLoadedEvent::class, new BasketItemsLoadedEvent($this));
        return $this->items;
    }

    /**
     * Update basket item quantities based on request values
     *
     * @param array $request request with item details
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function updateQuantities($request_items)
    {
        if (is_array($request_items)) {
            foreach ($request_items as $request_item) {
                if ($request_item['quantity'] > 0) {
                    $request_item['quantity'] = $this->enforceMaxQuantitySize($request_item['quantity']);
                    DB::table('basket_items')
                        ->where('basket_id', $this->id)
                        ->where('id', $request_item['id'])
                        ->update([
                            'quantity' => $request_item['quantity']
                        ]);
                } else {
                    $this->deleteItem($request_item['id']);

                }
            }
            $this->resetSession();
        }
    }

    /**
     * Remove item from basket
     *
     * @param int $delete_id item id to delete
     * @return void
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function deleteItem($delete_id): void
    {
        Event::dispatch(BasketRemoveItemEvent::class, new BasketRemoveItemEvent($delete_id));

        DB::table('basket_items')
            ->where('basket_id', $this->id)
            ->where('id', $delete_id)
            ->delete();
        $this->resetSession();
    }


    /**
     * Validate Coupon
     * All functionality moved to Coupon class
     *
     * @return
     */
    public function Validate_Coupon($code)
    {
        if (sizeof($this->items) == 0) {
            $this->Get_Items();
        }
        return Coupon::validate($code, $this);
    }


    /**
     * Basket::Set_Coupon().
     *
     * @param string $code
     * @return bool
     */
    public function Set_Coupon(string $code): bool
    {
        if (!$this->Validate_Coupon($code)) {
            return false;
        }

        \Mtc\Shop\Basket::query()
            ->where('id', $this->id)
            ->update([
                'coupon' => $code,
            ]);

        $this->generateCouponMessage();
        $this->resetSession();
        return true;
    }

    /**
     * Clear the current coupon for basket
     *
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function removeCoupon()
    {
        DB::table('basket')
            ->where('id', $this->id)
            ->update([
                'coupon' => ''
            ]);
        $this->resetSession();
    }

    /*
    Delivery Functions
    */

    /**
     * Basket::Get_Delivery_Choices().
     *
     * @return
     */
    public function Get_Delivery_Choices()
    {
        if (!$this->has_physical_items) {
            $this->delivery_options = collect([]);
            return;
        }
        $weight = $this->weight;
        $has_only_nhs_items = $this->hasOnlyNHSItems();
        $item_ids = collect($this->items)
            ->pluck('item_id');

        $has_doctor_items = 0;
        if ($item_ids->count()) {
            $has_doctor_items = Mtc\Shop\Category::query()
                ->whereHas('items', function ($query) use ($item_ids) {
                    return $query->whereIn('items.id', $item_ids);
                })
                ->where('is_online_doctor', 1)
                ->count();
        }

        $this->zone = $this->get_country_zone();

        // Get all items that have delivery methods restricted
        $itemsWithRestrictedDM = \Mtc\Shop\Item::query()
            ->with('deliveryMethods')
            ->whereIn('items.id', $item_ids)
            ->whereHas('deliveryMethods')
            ->get();

        $this->delivery_options = DeliveryMethod::query()
            ->where('disable', 0)
            ->where('heavy', $this->delivery_heavy)
            ->where('zone', $this->zone)
            ->orderBy('order', 'asc')
            ->orderBy('cost', 'asc')
            ->get()
            ->reject(function ($method) use ($weight, $has_doctor_items, $itemsWithRestrictedDM) {

                // Don't allow sending Doctor items to Zone 3 (outer EU)
                if ($method->zone === 3 && $has_doctor_items) {
                    return true;
                }

                // For any item with restricted delivery methods - ones that are not enabled
                // should be removed for all basket.
                // Works like exclusion
                /** @var \Mtc\Shop\Item $itemWithRestrictedDM */
                foreach ($itemsWithRestrictedDM as $itemWithRestrictedDM) {
                    $allowedDeliveryMethodIds = $itemWithRestrictedDM->deliveryMethods
                        ->pluck('id')
                        ->toArray();
                    if (!in_array($method->id, $allowedDeliveryMethodIds)) {
                        return true;
                    }
                }

                // END T85052 - Amending delivery methods based on basket contents

                // Find if there are any rates for this method with this basket weight
                $rate_exists = \Mtc\Shop\DeliveryMethodRate::query()
                        ->where('method_id', $method->id)
                        ->where('weight_min', '<=', $weight)
                        ->where('weight_max', '>=', $weight)
                        ->count() > 0;

                if (!$rate_exists) {
                    $lower_rate_exists = \Mtc\Shop\DeliveryMethodRate::query()
                            ->where('method_id', $method->id)
                            ->where('weight_max', '<', $weight)
                            ->count() > 0;

                    return $lower_rate_exists;
                }

                return false;
            })
            ->each(function ($delivery_method) use ($has_only_nhs_items) {
                if ($delivery_method->id === 1 && $has_only_nhs_items) {
                    $delivery_method->cost = 0;
                }
                return $delivery_method;
            })
            ->values()
            ->toArray();

        Event::dispatch(__METHOD__, $this);
    }

    /**
     * Retrieve currency rates.
     *
     * @return void
     */
    public function getCurrencyRates(): void
    {
        $rates = Currency::all();
        foreach ($rates as $rate) {
            $this->c_rates[$rate->currency] = $rate->ratio;
        }

        // set active currency on basket
        if (isset($_SESSION)) {
            $this->currency = $_SESSION['currency'];
        }
    }

    /**
     * Basket::Set_Delivery_Cost().
     *
     * @return void
     */
    public function Set_Delivery_Cost(): void
    {
        \Mtc\Shop\Basket::query()
            ->where('id', $this->id)
            ->update([
                'delivery' => $this->delivery_selected,
                'delivery_instructions' => $this->delivery_instructions,
            ]);
        $this->resetSession();
    }

    /**
     * Basket::Calculate_Delivery_Cost().
     *
     * @return
     */
    public function Calculate_Delivery_Cost()
    {
        if (!defined('DELIVERY_COST') || empty(constant('DELIVERY_COST'))) {
            return;
        }

        $num_items = 0;
        foreach ($this->items as $line) {
            $num_items += $line['quantity'];
        }

        // Gather all delivery costs and find the selected one
        $options = new Collection($this->delivery_options);
        if ($options->count() === 0) {
            return;
        }
        $selected = $options->where('id', $this->delivery_selected)->first() ?: $options->first();

        $this->delivery_selected = 0;
        $this->cost_delivery = 0;

        if ($this->has_physical_items) {
            // set the variables for delivery selected and cost
            $this->delivery_selected = $selected['id'] ?: 0;
            $this->cost_delivery = $selected['cost'];
        }

        // Find if there are any rates for this method with this basket weight
        $rate = \Mtc\Shop\DeliveryMethodRate::query()
            ->where('method_id', $selected['id'])
            ->where('weight_min', '<=', $this->weight)
            ->where('weight_max', '>=', $this->weight)
            ->first();

        if ($rate) {
            // line cost has a fixed price, otherwise we have per kg cost
            if ($rate->line_cost) {
                $this->cost_delivery = $rate->line_cost;
            } else {
                $this->cost_delivery += ceil($this->weight - $rate->weight_min) * $rate->kg_cost;
            }
        }

        // Check if free delivery applies
        if ($selected['max'] < $this->cost_subtotal) {
            $this->cost_delivery = 0;
        }

        $this->Set_Delivery_Cost();
        $this->cost_vat += $this->cost_delivery - $this->cost_delivery / (1 + VAT_RATE / 100);
    }

    //Work Out Final Costs

    /**
     * Basket::Total_Costs().
     *
     * @return
     */
    public function Total_Costs()
    {
        $cost = $this->cost_subtotal + $this->cost_delivery - $this->coupon_deduct;

        $this->cost_total = $cost;

        if ($this->cost_total < 0) {
            $this->cost_total = 0;
        }

        $cost = $this->cost_subtotal_exvat;
        $cost = $cost + $this->cost_delivery;
        $this->cost_total_exvat = $cost;
    }

    /**
     * Basket::Stock_Check().
     *
     * @return void
     */
    public function Stock_Check(): void
    {
        if (empty($this->items)) {
            return;
        }
        foreach ($this->items as $key => $basketItem) {
            if ($basketItem['sizeid'] > 0) {
                $size = Size::query()
                    ->find($basketItem['sizeid']);
                $stock = $size->stock ?? 0;
            } else {
                $item = \Mtc\Shop\Item::query()
                    ->find($basketItem['item_id']);
                $stock = $item->stock ?? 0;
            }

            if ($basketItem['quantity'] > $stock) {
                $this->stock_error[$basketItem['id']] = 'There is currently only ' . $stock . ' of this item in stock';
                $this->instock = false;

                $this->items[$key]['stock_error'] = 'There is currently only ' . $stock . ' of this item in stock';
            }
        }
    }

    /**
     * Basket::Set_Member().
     *
     * @param $memberID
     * @return void
     */
    public function Set_Member($memberID): void
    {
        \Mtc\Shop\Basket::query()
            ->where('id', $this->id)
            ->update([
                'member' => $memberID,
            ]);
        $this->resetSession();
    }

    /**
     * Function stores customer info
     * This handles both adding and updating info
     * Upgraded to Eloquent DB facade for ease of use
     *
     * @param array $request details to save
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function setCustomerInfo($request)
    {
        foreach ($request as $key => $value) {
            $request[$key] = strip_tags($value);
            if ($request[$key] === 'true') {
                $request[$key] = 1;
            }

            if ($key == 'assessment_ids' && empty($value)) {
                $request[$key] = [];
            }
        }

        if (empty($request['id'])) {
            $request['basket_id'] = $this->id;
            $info = BasketInfo::create($request);
            $request['id'] = $info->id;
        } else {
            BasketInfo::where('basket_id', $this->id)
                ->where('id', $request['id'])
                ->update($request);
        }
        $this->info = $request;
    }

    /**
     * Retrieve Basket Info array
     * Contains mostly contact or marketing information
     * For legacy retrieves data as array instead of object
     *
     * @author Martins.Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */

    function Get_CustomerInfo()
    {
        $info = BasketInfo::where('basket_id', $this->id)
            ->orderBy('id', 'DESC')
            ->first();

        $this->info = $info ? $info->toArray() : [];
    }

    /**
     * Basket::Get_CustomerAddress().
     *
     * @return void
     */
    public function Get_CustomerAddress(): void
    {
        $addresses = Address::query()
            ->where('basket_id', $this->id)
            ->get();
        foreach ($addresses as $address) {
            $address = $address->toArray();
            $this->address[$address['type']] = $address;
        }

        if ($this->billing_address_used || empty($this->address['shipping'])) {
            $this->address['shipping'] = $this->address['billing'];
            $this->address['shipping']['type'] = 'shipping';
            unset($this->address['shipping']['id']);
        }

        $this->checkIfNoVATBasket();
    }

    /**
     * Check if basket qualifies as no_vat basket.
     * Determined by users billing address
     *
     * return bool
     */
    private function checkIfNoVATBasket()
    {
        if (DISCOUNT_VAT !== true) {
            return false;
        }

        // Address yet not set - not viable for discounted vat
        if (empty($this->address['shipping']['country'])) {
            return false;
        }

        // EU countries have VAT. UK exemption is processed later
        if (Country::isEu($this->address['shipping']['country'])
            && $this->address['shipping']['country'] != 'GB'
        ) {
            return false;
        }

        // Only allow JE & GY postcodes for UK based no_vat zones
        $postcode = strtoupper(substr($this->address['shipping']['postcode'], 0, 2));
        if ($this->address['shipping']['country'] == 'GB'
            && !in_array($postcode, array('JE', 'GY'))
        ) {
            return false;
        }

        $this->no_vat = true;
        $this->cost_subtotal = $this->cost_subtotal_exvat;
        return true;
    }

    /**
     * Function stores customer address by address type
     * This handles both adding and updating address
     *
     * @param string $type address type
     * @param array $request details to save
     * @return void
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function setCustomerAddress($type, $request): void
    {
        //clean data
        foreach ($request as $key => $value) {
            $request[$key] = strip_tags($value);
        }

        $request['type'] = $type;
        if (empty($request['id'])) {
            $request['basket_id'] = $this->id;
            $address = Mtc\Shop\Basket\Address::query()
                ->create($request);
            $address->basket_id = $request['basket_id'];
            $address->save();
            $request['id'] = $address->id;
        } else {
            $address = Mtc\Shop\Basket\Address::query()
                ->find($request['id']);
            if (!$address) {
                $address = new Mtc\Shop\Basket\Address();
            }
            $address->fill($request);
            $address->basket_id = $request['basket_id'];
            $address->save();
        }
        $this->address[$type] = $request;
    }

    /**
     * Basket::Add_CustomerAddress().
     *
     * @return void
     */
    public function Add_CustomerAddress(): void
    {
        foreach ($this->address as $type => $data) {
            if (empty($data['id'])) {
                $data['type'] = $type;
                $data['address3'] = $data['address3'] ?? '';
                $data['address4'] = $data['address4'] ?? '';
                $data['address5'] = $data['address5'] ?? '';

                $createdAddress = Address::query()
                    ->create($data);

                $this->address[$type]['id'] = $createdAddress->id;
            }
        }
    }

    /**
     * Add / Update order notes added on basket page.
     *
     * @author Ilya Berdnikov
     */
    public function updateOrderNotes()
    {
        BasketInfo::where('basket_id', $this->id)
            ->update([
                'message' => $this->info['message']
            ]);
    }

    /**
     * Basket::Basket_Html()
     *
     * @return
     */
    public function Basket_Html()
    {
        ob_start();

        $basket = new Order($this->order_id);
        $preorder = false;
        include SITE_PATH . '/shop/checkout/templates/email.basket.php';

        $this->basket_html = ob_get_clean();
    }

    /**
     * Basket::Get_Total().
     *
     * @return
     */
    public function Get_Total()
    {
        $total = 0;
        $item = new Item();
        if (sizeof($this->items) > 0) {
            foreach ($this->items as $i) {
                $item->Get_Item($i['item_id']);
                $total += ($item->price * $i['quantity']);
            }
        }

        return $total;
    }

    /**
     * Basket::Get_Number_Of_Items().
     *
     * @return
     */
    public function Get_Number_Of_Items()
    {
        $total_items = 0;
        if (sizeof($this->items) > 0) {
            foreach ($this->items as $line) {
                $total_items += $line['quantity'];
            }
        }

        return $total_items;
    }

    /**
     * Basket::set_expiry_time().
     *
     * @return void
     */
    public function set_expiry_time(): void
    {
        $basket_expiry_time = time() + self::$duration;
        \Mtc\Shop\Basket::query()
            ->where('id', $this->id)
            ->update([
                'basket_expiry_time' => $basket_expiry_time,
            ]);
    }

    //Protx

    /**
     * Basket::ProtxTx().
     *
     * @return void
     */
    public function ProtxTx(): void
    {
        $protx = Protx::query()
            ->create([
                'basket_id' => $this->id,
                'price' => $this->cost_total,
                'date' => Carbon::now(),
            ]);

        $this->protx = $protx->id;
    }

    /**
     * Whenever discounts are applied the VAT amount changes.
     * This needs to be reflected by calculating the vat cost for basket based on prices after discounts
     * Voucher discounts are treated as "money off" vouchers, and so do not use any VAT reduction
     *
     * @author mtc.
     */
    public function vat_reduction()
    {
        if ($this->coupon_percentoff > 0 || !empty($this->discounts_found)) {
            // do reduction
            // set total VAT to delivery VAT amount
            $this->cost_vat = $this->cost_delivery - $this->cost_delivery / (1 + VAT_RATE / 100);
            foreach ($this->items as $k => $line) {
                $percent_off = $this->coupon_percentoff;
                if (!isset($line['percentage_discount_applied'])) {
                    $percent_off = 0;
                }

                $new_price = $line['afterdiscount_item_price'] * (1 - $percent_off / 100);
                $new_price_exvat = $new_price;

                if ($line['vat_deductable']) {
                    $new_price_exvat = $new_price / (1 + $line['vat_rate'] / 100);
                }

                $this->items[$k]['price_paid'] = $new_price;
                $this->items[$k]['price_paid_exvat'] = $new_price_exvat;
                // add item VAT to total VAT
                $this->cost_vat += ($new_price - $new_price_exvat) * $line['quantity'];
            }
        }
    }


    /**
     * Apply multi-buy discounts
     *
     * @author Alan Reid
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    function Discounts()
    {
        $this->total_discount = 0;
        DiscountOffer::applyOnBasket($this);

        foreach ($this->discounts_found as $discount) {
            $this->total_discount = $this->total_discount + $discount['discount_amount'];
        }

        $this->cost_subtotal_beforediscounts = $this->cost_subtotal;
        $this->cost_subtotal = $this->cost_subtotal - $this->total_discount;

        //Reduce the price of items based on the discount
        if ($this->total_discount > 0) {
            //Calculate discount percent compared to order.
            $percent_off = ($this->total_discount / $this->cost_subtotal_beforediscounts) * 100;
            $this->cost_vat = $this->cost_delivery - $this->cost_delivery / (1 + VAT_RATE / 100);
            foreach ($this->items as $k => $line) {
                $new_price = $line['item_price'] * (1 - $percent_off / 100);
                $new_price_ex_vat = $new_price;

                if ($line['vat_deductable']) {
                    $new_price_ex_vat = $new_price / (1 + $line['vat_rate'] / 100);
                }
                $this->items[$k]['afterdiscount_item_price'] = $new_price;
                $this->items[$k]['afterdiscount_item_price_exvat'] = $new_price_ex_vat;

                $this->items[$k]['afterdiscount_line_total'] = $new_price * $line['quantity'];
                $this->items[$k]['afterdiscount_line_total_exvat'] = $new_price_ex_vat * $line['quantity'];

                // add item VAT to total VAT
                $this->cost_vat += ($new_price - $new_price_ex_vat) * $line['quantity'];
            }
        }
    }

    /**
     * Basket::get_country_zone().
     *
     * @return string
     */
    function get_country_zone()
    {
        if (!empty($this->address['shipping']['country']) && empty($_SESSION['ship_to_billing_address'])) {
            $country_code = $this->address['shipping']['country'];
        } elseif (!empty($this->address['billing']['country'])) {
            $country_code = $this->address['billing']['country'];
        } else {
            $country_code = isset($_SESSION['country']) ? $_SESSION['country'] : null;
        }

        if (empty($country_code)) {
            $country_code = 'GB';
        }
        $zone = DeliveryZone::query()
            ->where('country_code', $country_code)
            ->where('disable', 0)
            ->first();

        return !empty($zone) ?
            $zone->zone :
            '';
    }

    /*
    * Basket::generateCouponMessage())
    *
    * Generates a friendly coupon message to display on the basket
    */
    public function generateCouponMessage()
    {
        $this->coupon_message = '';

        if ($this->coupon_code != '') {
            $this->coupon_message .= 'Discount Code "' . $this->coupon_code . '" gives you ';

            if ($this->coupon_freedelivery) {
                $this->coupon_message .= 'free delivery';
            }

            if ($this->coupon_freedelivery && ($this->coupon_amountof || $this->coupon_percentoff)) {
                $this->coupon_message .= ' and ';
            }

            if ($this->coupon_amountof) {
                if (ENABLE_CURRENCIES && $_SESSION['currency'] != '') {
                    $this->coupon_message .= $curr_prefix['symbol'];
                } else {
                    $this->coupon_message .= '£';
                }

                $this->coupon_message .= $this->coupon_amountof . ' off';
            }

            if ($this->coupon_percentoff) {
                $this->coupon_message .= $this->coupon_percentoff . '% off';
            }
        }

        // Allow external sources to modify coupon message if other rules apply
        Event::dispatch('Basket::generateCouponMessage', $this);
    }

    /**
     * Retrieve basket required info
     *
     * @return array list of required fields
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function getRequired()
    {
        $required = [
            'address' => [
                'firstname',
                'lastname',
                'address1',
                'city',
            ],
            'info' => [
                'email',
                'contact_no'
            ]
        ];

        if (!empty($this->contains_doctor_pharmacy_items)) {
            $required['info'][] = 'dob_date';
            $required['info'][] = 'dob_month';
            $required['info'][] = 'dob_year';
        }

        if (TERMS_SHOW === true) {
            $required[] = 'terms';
        }
        return $required;

    }

    /**
     * Validates basket to check if user can checkout
     * Expects the following structure to be present in request parameters
     * [
     *      basket => Basket object as an array
     *      terms => whether terms are checked (if TERMS_SHOW setting)
     *      optional_password => user sign-up password (if non-member and has MEMBERS_BASKET)
     *      ship_to_billing_address => whether use billing or shipping address for delivery
     * ]
     *
     * @param array $request request to validate
     * @param array $basket_countries list of allowed basket countries
     * @param array $state_list List of states grouped by country
     * @return array list of validation errors
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function validate($request, $basket_countries, $state_list)
    {
        $errors = [];
        $required_fields = $this->getRequired();
        if (in_array('terms', $required_fields) && empty($request['terms'])) {
            $errors['terms'] = "You must agree to the terms and conditions to continue";
        }

        // validate & clean info fields
        $errors = array_merge($errors, $this->validateInfo($required_fields));

        // validate & clean addresses
        $same_address = $request['basket']['billing_address_used'] == 1;
        $errors = array_merge($errors, $this->validateAddress($required_fields, $state_list, $same_address));

        // validate delivery
        $errors = array_merge($errors, $this->validateDelivery($basket_countries));

        // if is set coupon 'Apply only to orders by new customers'
        if (COUPONS_ADVANCED === true
            && !empty($this->coupon_code)
            && !empty($this->coupon_first_order)
            && has_ordered($this->info['email'], false, $this->id)
        ) {
            $errors['coupon_error'] = 'This coupon only applies to your first order.';
        }

        if ($this->stock_error) {
            $errors['stock'] = 'Insufficient stock for some items in basket';
        }

        if (!empty($this->delivery_error)) {
            $errors['delivery'] = 'Unfortunately we are unable to post prescription medication to your location due to local laws. Read more about deliveries <a href="/customer-care/delivery/">here.</a>';
        }

        if (!empty($this->limit_error)) {
            $errors['limit'] = 'One or more items in your basket has exceeded the purchase limit';
        }

        if (!empty($this->general_error)) {
            $errors['general'] = $this->general_error;
        }

        if (!empty($this->incompatibility_error)) {
            $errors['limit'] = 'The medication in the basket have restrictions. Please see the list of medication for more information.';
        }

        if (!empty($this->require_id_check_cascade)) {
            if (empty($_SESSION['ID_CHECK_TYPE'])) {
                $errors['info']['id_type'] = 'Please specify the ID Document Type';
            }
            if (empty($_SESSION['ID_CHECK_NUMBER'])) {
                $errors['info']['id_number'] = 'Please specify the ID Number';
            }
        }

        return $errors;
    }

    /**
     * Validates and cleans info fields for basket
     *
     * @param array $required required fields
     * @return array
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    private function validateInfo($required)
    {
        $errors = [];

        foreach ($required['info'] as $required_field) {
            if (empty($this->info[$required_field])) {
                if (in_array($required_field, ['dob_date', 'dob_month', 'dob_year'])) {
                    $errors['info']['dob'] = 'Date of Birth is not valid';
                }
                $errors['info'][$required_field] = 'This field is required';
            }
        }

        if (!filter_var($this->info['email'], FILTER_VALIDATE_EMAIL)) {
            $errors['info']['email'] = 'Please enter a valid email address';
        }

        // if we contain doctor / pharmacy items in the basket we do require DOB fields to be present and valid
        if ($this->contains_doctor_pharmacy_items) {
            try {
                $dob_string = $this->info['dob_year']
                    . '-' . str_pad($this->info['dob_month'], 2, 0, STR_PAD_LEFT)
                    . '-' . str_pad($this->info['dob_date'], 2, 0, STR_PAD_LEFT);
                $date = \Carbon\Carbon::create($this->info['dob_year'], $this->info['dob_month'], $this->info['dob_date']);
                if ($date->format('Y-m-d') != $dob_string) {
                    $errors['info']['dob'] = 'The entered date of birth is not valid';
                } elseif (\Carbon\Carbon::now()->subYears(18) < $date) {
                    $errors['info']['dob'] = 'You must be at least 18 years old to make this purchase';
                } elseif (\Carbon\Carbon::now()->subYears(120) > $date) {
                    $errors['info']['dob'] = 'The entered date of birth is not valid';
                }
            } catch (\InvalidArgumentException $exception) {
                $errors['info']['dob'] = 'Please enter a valid date of birth';

            }
        }

        // remove + or 00 from the prefix and make sure number is trimmed
        // so we can validate against the country dial codes in DB

        // Commented out per client's request.
        //$cleaned_prefix = trim(str_replace(['+', '00'], '', $this->info['phone_prefix']));
        //if (Country::query()->where('dial_code', $cleaned_prefix)->count() == 0) {
        //    $errors['info']['phone_prefix'] = 'Phone prefix was not recognized';
        //}

        if (!preg_match(validate::PHONE_REGEX, $this->info['contact_no'])) {
            $errors['info']['contact_no'] = 'Please enter a valid phone number';
        }

        return $errors;
    }

    /**
     * Validates and cleans address for basket
     *
     * @param array $required required fields
     * @param array $state_list list of states grouped by country
     * @return array
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    private function validateAddress($required, $state_list, $same_address)
    {
        $errors = [];

        foreach ($required['address'] as $required_field) {
            if (empty($this->address['billing'][$required_field])) {
                $errors['address']['billing'][$required_field] = 'This field is required';
            }
            if (empty($this->address['shipping'][$required_field])) {
                $errors['address']['shipping'][$required_field] = 'This field is required';
            }
        }

        $first_name_words = explode(' ', $this->address['billing']['firstname']);
        foreach ($first_name_words as $word) {
            if (strlen($word) == 1) {
                $errors['address']['billing']['firstname'] = 'Please enter your full First Name. Initials are not allowed';
            }
        }

        $last_name_words = explode(' ', $this->address['billing']['lastname']);
        foreach ($last_name_words as $word) {
            if (strlen($word) == 1) {
                $errors['address']['billing']['lastname'] = 'Please enter your full Last Name. Initials are not allowed';
            }
        }

        foreach (array_keys($this->address) as $address_type) {
            if (
                $this->missingDifferentAddressReason($same_address, $address_type) &&
                !BasketBuilder::isBuilderBasket($this->id)
            ) {
                $errors['address'][$address_type]['notes'] = 'This field is required';
            }

            //strip superfluous spaces from postcodes to avoid validation failing needlessly
            $the_postcode = $this->address[$address_type]['postcode'];
            $the_postcode = trim($the_postcode);
            $the_postcode = preg_replace('/\s+/', ' ', $the_postcode);
            $this->address[$address_type]['postcode'] = $the_postcode;

            // Validate Postcode
            // Test if country has a postcode system
            if (Country::hasPostcodes($this->address[$address_type]['country'])) {
                if (empty($this->address[$address_type]['postcode'])) {
                    $errors['address'][$address_type]['postcode'] = 'This field is required';
                } elseif (!PostcodeFactory::build($this->address[$address_type]['postcode'],
                    $this->address[$address_type]['country'])->validates()
                ) {
                    $errors['address'][$address_type]['postcode'] = 'Postcode must be a valid ' . Country::getNameByCode($this->address[$address_type]['country']) . ' postcode';
                } elseif (!empty($state_list[$this->address[$address_type]['country']])
                    && empty($this->address[$address_type]['state'])
                ) {
                    /*
                     *  Check if country has a state defined
                     * If it has check if it is not empty and mark as error if empty
                     */
                    $errors['address'][$address_type]['state'] = 'This field is required';
                }
            } else {
                $this->address[$address_type]['postcode'] = "";
            }


        }

        return $errors;

    }

    /**
     * @param $same_address
     * @param $type
     * @return bool
     */
    private function missingDifferentAddressReason($same_address, $type)
    {
        return $same_address === false
            && $type === 'shipping'
            && empty($this->address[$type]['notes']);
    }

    /**
     * Validates basket to check if there is a valid delivery method for current basket.
     * First finds zone for current delivery. After that matches against existing zone mapping
     * if chosen method is available to the zone.
     *
     * @param array $basket_countries list of countries allowed in basket
     * @return array list of validation errors
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    private function validateDelivery($basket_countries)
    {
        if (!$this->has_physical_items) {
            return [];
        }
        if ($this->delivery_selected == 0) {
            return [
                'delivery' => [
                    "There is no delivery option available. Please contact us for delivery options."
                ]
            ];
        }
        $failed_region = '';
        // Assigning this to a variable for shorthand
        $shipping = $this->address['shipping'];

        $check_zone = DB::table('delivery_methods')
            ->where('id', $this->delivery_selected)
            ->first();
        $zone = $check_zone->zone;
        // Postal code override to narrow down zone
        if (!empty($check_zone->subzone)) {
            $zone = $check_zone->subzone;
        }

        // if current zone is stored as a postal code zone
        if ($GLOBALS['postal_zones'][$zone] == 'postcode') {
            // if not UK but a postal code zone [requires UK] throw an error
            if ($shipping['country'] != 'GB') {
                $failed_region = $basket_countries[$shipping['country']];

            } elseif (!empty($shipping['postcode'])) {
                // if postcode present check if it matches zone restrictions
                if (!$this->checkZone(${'zone' . $zone}, $shipping['postcode'])) {
                    $failed_region = $shipping['postcode'];
                }
            }
        } elseif ($shipping['country'] == 'GB') {
            //check it against any postcode arrays on file.
            $all_postcode_zones = array();

            foreach ($GLOBALS['postal_zones'] as $key => $value) {
                if ($value == 'postcode') {
                    $all_postcode_zones = array_merge($all_postcode_zones, ${'zone' . $key});
                }
            }

            if ($this->checkZone($all_postcode_zones, $shipping['postcode'])) {
                $failed_region = $shipping['postcode'];
            }
        }

        if (!empty($failed_region)) {
            return [
                'delivery' => "The selected delivery option is not available to <b>" . $failed_region . "</b>!"
            ];
        }
        return [];
    }

    /**
     * Check if postcode provided is withing given uk postcode zones
     *
     * @param array $postcode_zones allowed postcode zones
     * @param string $postcode postcode to check
     * @return bool whether postcode is within zones allowed
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    private function checkZone($postcode_zones, $postcode)
    {
        foreach (array_keys($postcode_zones) as $postcode_prefix) {
            $postcode = str_replace(' ', '', strtoupper($postcode));
            $prefix_length = strlen($postcode_prefix);
            $outer_prefix = substr($postcode, 0, $prefix_length);

            // If the first part of the postcode matches a postcode zone
            if ($outer_prefix == $postcode_prefix) {
                // If the postcode zone has an additional restrictions [8..10]
                if (sizeof($postcode_zones[$postcode_prefix]) > 0) {
                    foreach ($postcode_zones[$postcode_prefix] as $range) {
                        $floor = $range[0];
                        $ceil = $range[1];

                        $outer_postcode = substr($postcode, 0, -3);
                        for ($i = $floor; $i <= $ceil; $i++) {
                            if ($postcode_prefix . $i == $outer_postcode) {
                                return true;
                            }
                        }
                    }
                } else {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Handle subscription to newsletter in checkout
     *
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function subscribeToNewsletter()
    {
        $has_subscribed = DB::table('emails_lists_mails')
            ->where('email', $this->info['email'])
            ->count();
        if ($has_subscribed == 0) {
            DB::table('emails_lists_mails')
                ->insert([
                    'list_id' => 2, // Default list for Customers mailing list
                    'email' => $this->info['email'],
                    'firstname' => $this->address['billing']['firstname'],
                    'surname' => $this->address['billing']['lastname']
                ]);
        }
    }

    /**
     * Update the billing_address_used variable with new value
     * Changes which address is used as delivery address
     *
     * @param int $new_value 1 or 0 - whether billing address is used as delivery address
     * @return null
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function updateBillingAddressUsed($new_value)
    {
        DB::table('basket')
            ->where('id', $this->id)
            ->update([
                'billing_address_used' => (int)$new_value
            ]);
        $this->resetSession();
    }

    /**
     * Save basket info to DB.
     * This saves billing/shipping address, info and delivery_selected
     *
     * @param array $request $_POST request
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public function saveDetails($request)
    {
        if (!empty($request['address']['billing'])) {
            $this->setCustomerAddress('billing', $request['address']['billing']);
        }

        // Only save shipping address if it is set to be edited
        if (isset($request['billing_address_used'])
            && $request['billing_address_used'] == 0
            && !empty($request['address']['shipping'])
        ) {
            $this->setCustomerAddress('shipping', $request['address']['shipping']);
        }

        if (!empty($request['info']) && $request['info'] != 'false') {
            $this->setCustomerInfo($request['info']);
        }

        if (isset($request['delivery_selected'])) {
            $this->delivery_selected = (int)$request['delivery_selected'];

            if (isset($request['delivery_instructions'])) {
                $this->delivery_instructions = clean_page($request['delivery_instructions']);
            }

            $this->Set_Delivery_Cost();
        }

        $this->resetSession();
    }

    /**
     * Check if basket has a paid order attached to it
     * @param int $basket_id basket ID
     * @return int whether the basket has a paid order
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     */
    public static function isPaid($basket_id)
    {
        return DB::table('order')
            ->where('paid', 1)
            ->where('basket_id', $basket_id)
            ->count();
    }

    /**
     * Enforce max quantity size for items being allowed to be added to the basket
     * @param int $quantity number of items we want to add to the basket
     * @return int number of items always less than or equal to MAX_QUANTITY_SIZE
     * @author Reinis Liepkalns <reinis.liepkalns@mtcmedia.co.uk>
     */
    private static function enforceMaxQuantitySize($quantity)
    {
        if ($quantity > MAX_QUANTITY_SIZE) {
            return MAX_QUANTITY_SIZE;
        }
        return $quantity;
    }


    /**
     * Basket state checking
     * Used to used to check if a basket has states associated with the country
     *
     * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
     * @version 0.1 30/07/16
     */
    public function getBasketStateList()
    {
        $basket_state_list = [];
        if (empty($this->address)) {
            return [];
        }
        foreach ($this->address as $address_type => $address) {

            if (!empty($address['country'])) {
                $country_state_list = CountryState::getStateList($address['country']);
                if (!empty($country_state_list)) {
                    $basket_state_list[$address_type] = $country_state_list;
                }
            }

        }


        return $basket_state_list;
    }

    /**
     * Basket postcode checking
     * Used to used to check if a basket country uses postcodes
     *
     * @return array
     * @author  Jack Donaldson <jack.donaldson@mtcmedia.co.uk>
     * @version 11/04/17
     */
    public function getBasketHasPostcodes()
    {
        $basket_has_postcodes = [];

        if (empty($this->address)) {
            return [];
        }

        $billing_country = '';
        $shipping_country = '';

        if (!empty($this->address['billing']['country'])) {
            $billing_country = $this->address['billing']['country'];
        }

        if (!empty($this->address['shipping']['country'])) {
            $shipping_country = $this->address['shipping']['country'];
        }

        $basketAddresses = Address::query()
            ->leftJoin('countries', 'basket_address.country', '=', 'countries.code')
            ->where('basket_id', $this->id)
            ->whereIn('code', [$billing_country, $shipping_country])
            ->where('has_postcodes', 1)
            ->get();

        foreach ($basketAddresses as $basketAddress) {
            $basket_has_postcodes[$basketAddress->type] = true;
        }

        return $basket_has_postcodes;
    }

    /**
     * Validates restricted zones for the items
     * @return bool
     */
    public function validateRestrictedZones()
    {
        unset($this->delivery_error);
        if (!$delivery_method = (new DeliveryMethod())->find($this->delivery_selected)) {
            return true;
        }
        if (count($this->items) > 0) {
            foreach ($this->items as $key => $basket_item) {
                unset($this->items[$key]['delivery_error']);
                $restricted = (new \Mtc\Shop\Item())
                    ->with('categories')
                    ->find($basket_item['item_id']);

                if ($restricted) {
                    $restricted = $restricted->categories
                            ->map(function ($category) use ($delivery_method) {
                                if ($category->restricted_zones()->where('zone', $delivery_method->zone)->count()) {
                                    return true;
                                }
                                return false;
                            })
                            ->filter()
                            ->count() > 0;
                }
                if ($restricted) {
                    $this->delivery_error[$basket_item['id']] = 'Unfortunately we are unable to post prescription medication to your location due to local laws. Read more about deliveries <a href="/customer-care/delivery/">here.</a>';
                    $this->items[$key]['delivery_error'] = 'Unfortunately we are unable to post prescription medication to your location due to local laws. Read more about deliveries <a href="/customer-care/delivery/">here.</a>';
                }
            }
        }
        return empty($this->delivery_error);
    }

    /**
     * Check if basket quantity does not exceed purchase limits set for products
     * @return bool
     */
    public function validateIngredientLimits()
    {
        // Finds all orders based on the basket data. If member logged in, looks up their orders
        // and adds any that match by address
        $orders = \Mtc\Shop\Order::findPersonOrders(($_SESSION['member_id'] ?? null), $this->info, $this->address['shipping']);
        $ingredient_order_history = \Mtc\Shop\Order::getIngredientPurchaseHistory($orders);
        $item_order_history = \Mtc\Shop\Order::getItemPurchaseHistory($orders);

        unset($this->limit_error);
        // no items - no errors
        if (count($this->items) == 0) {
            return true;
        }

        $basket = $this;
        $basket_item_ids = collect($this->items)->pluck('item_id');

        // Find all ingredients that are assigned to products in basket
        // and check if limits are exceeded
        Ingredient::query()
            ->with('items')
            ->whereHas('items', function ($query) use ($basket_item_ids) {
                $query->whereIn('items.id', $basket_item_ids);
            })
            ->get()
            ->each(function (Ingredient $ingredient) use ($basket, $ingredient_order_history) {

                // Find which items have this ingredient and the sum amount of products in basket for this ingredient
                $items_in_basket = collect($basket->items)->whereIn('item_id', $ingredient->items->pluck('id'));
                $ingredient->amount_in_basket = $items_in_basket->sum('quantity');

                if (!$ingredient->exceedsIngredientLimit($items_in_basket, $ingredient_order_history[$ingredient->id] ?? [])) {
                    return;
                }

                // Limit exceeded, set error
                $basket->setLimitError($items_in_basket->pluck('id'), $ingredient->message);
            });

        // Add in per item restrictions
        Mtc\Shop\Item::query()
            ->whereIn('id', collect($basket->items)->pluck('item_id'))
            ->get()
            ->reject(function ($item) {
                return empty($item->restriction_period_length)
                    && empty($item->restriction_per_period)
                    && empty($item->restriction_per_order)
                    && empty($item->restriction_limit_once);
            })
            ->each(function ($item) use ($basket, $item_order_history) {
                $items_in_basket = collect($basket->items)->whereIn('item_id', $item->id);

                $ingredient = new Ingredient();
                $ingredient->limit_period = $item->restriction_period_length;
                $ingredient->limit_amount = $item->restriction_per_period;
                $ingredient->limit_order = $item->restriction_per_order;
                $ingredient->limit_once = $item->restriction_limit_once;
                $ingredient->amount_in_basket = $items_in_basket->sum('quantity');
                if (!$ingredient->exceedsIngredientLimit($items_in_basket, $item_order_history[$item->id] ?? [])) {
                    return;
                }

                // Limit exceeded, set error
                $basket->setLimitError($items_in_basket->pluck('id'));
            });


        return empty($this->limit_error);
    }

    /**
     * Check if basket items have incompatible categories
     *
     * @return bool
     */
    public function validateIncompatibleCategories()
    {
        unset($this->incompatibility_error);
        // don't validate if only one item in the basket
        if (count($this->items) < 2) {
            return true;
        }

        $incompatibilities_added = [];
        // Loop through basket items.
        foreach ($this->items as $k => $basket_item) {
            // Loop through categories of each basket item
            foreach ($basket_item['categories'] as $category_id) {
                // Find all incompatibilities the category is involved. Ignore incompatibilities this item already has been added
                $incompatibilities = (new \Mtc\Shop\Incompatibility())
                    ->whereHas('categories', function (\Illuminate\Database\Eloquent\Builder $query) use ($category_id) {
                        $query->where('categories.id', '=', $category_id);
                    })
                    ->whereNotIn('incompatibilities.id', !empty($basket_item['incompatibilities']) ? (array)$basket_item['incompatibilities'] : [])
                    ->get();

                // For each incompatibility, try locating whether the items of incompatible categories are in the basket.
                foreach ($incompatibilities as $incompatibility) {
                    // Loop through incompatibility categories to look for the items of this category in basket
                    foreach ($incompatibility->categories as $incompatible_category) {
                        // Do not look at the same category unless only one category is added (incompatible items of same category)
                        if ((int)$incompatible_category->id === (int)$category_id && count($incompatibility->categories) > 1) {
                            continue;
                        }
                        foreach ($this->items as $k2 => $basket_item_iterated) {
                            // Do not look at the same item
                            if ($k === $k2) {
                                continue;
                            }
                            // If we find that other item has the incompatible category
                            if (in_array($incompatible_category->id, $basket_item_iterated['categories'], false)) {
                                // Add the incompatibilities so that they are not checked again for the same item
                                if (!in_array($incompatibility->id, $incompatibilities_added, false)) {
                                    $incompatibilities_added[] = $incompatibility->id;
                                    // Set error
                                    $this->setIncompatibilityError(
                                        [
                                            $basket_item['id'],
                                        ],
                                        $incompatibility->message
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }

        return empty($this->incompatibility_error);
    }

    /**
     * Set limit errors for products that
     * @param $basket_item_ids
     * @param string $message
     */
    public function setLimitError($basket_item_ids, string $message = '')
    {
        $message = $message ?: 'This item has exceeded limit';
        foreach ($basket_item_ids as $basket_item_id) {
            $this->limit_error[$basket_item_id] = $message;

            foreach ($this->items as $key => $item) {
                if ($item['id'] == $basket_item_id) {
                    $this->items[$key]['limit_error'] = $message;
                }
            }
        }
    }

    /**
     * Set limit errors for products that
     * @param $basket_item_ids
     * @param string $message
     */
    public function setIncompatibilityError($basket_item_ids, string $message = '')
    {
        $message = $message ?: 'These medication have restrictions. Please remove one of the medications in order to proceed.';
        foreach ($basket_item_ids as $basket_item_id) {
            $this->incompatibility_error[$basket_item_id][] = $message;

            foreach ($this->items as $key => $item) {
                if ((int)$item['id'] === (int)$basket_item_id) {
                    $this->items[$key]['incompatibility_error'][] = $message;
                }
            }
        }
    }

    /**
     * @param $type
     * @return void
     */
    public function addBasketFlag($type): void
    {
        \Mtc\Shop\Basket::query()
            ->where('id', $this->id)
            ->update([
                'flag_type' => $type,
            ]);
        $this->resetSession();
    }

    /**
     * Whether there are NHS items in the basket
     *
     * @return bool
     */
    public function hasNHSItems()
    {
        foreach ($this->items as $item) {
            if ($item['nhs_prescription']) {
                return true;
            }
        }
        return false;
    }

    /**
     * Whether there are only NHS items in the basket
     * There shouldn't be cases where NHS items mix with others, however, this function ensures that only NHS items
     * are in the basket
     *
     * @return bool
     */
    public function hasOnlyNHSItems()
    {
        foreach ($this->items as $item) {
            if (!$item['nhs_prescription']) {
                return false;
            }
        }
        return true;
    }

    /**
     * Find out whether verifications are required
     */
    public function initVerifications()
    {
        $this->verify_email = ACCOUNT_VERIFIER &&
            VERIFY_EMAIL &&
            \Mtc\Plugins\AccountVerifier\Classes\AccountVerifierSettings::query()->where('code', 'enable_email_verification')->where('value', '1')->count() > 0 &&
            !\Mtc\Plugins\AccountVerifier\Classes\AccountVerifier::emailVerified($this->info['email']);

        // Verify only if Global settings enabled, admin settings enabled and (member not created or not verified)
        $this->verify_phone = ACCOUNT_VERIFIER &&
            VERIFY_SMS &&
            \Mtc\Plugins\AccountVerifier\Classes\AccountVerifierSettings::query()->where('code', 'enable_phone_verification')->where('value', '1')->count() > 0
            &&
            !\Mtc\Plugins\AccountVerifier\Classes\AccountVerifier::phoneVerified($this->info['phone_prefix'] . $this->info['contact_no']);

        $this->updateSession();
    }

    public function hasPharmacistItems()
    {
        return collect($this->items)->where('product_type', '=', 'pharmacy')->count() > 0;
    }

    /**
     * Check basket
     * @return bool
     */
    public function hasDoctorItems()
    {
        return collect($this->items)->where('product_type', '=', 'doctor')->count() > 0;
    }

    /**
     * Check if basket contains items needing reviewed
     *
     * @return bool
     */
    public function containsItemsNeedingReview()
    {
        return $this->hasDoctorItems() || $this->hasPharmacistItems();
    }


    public function eloquent()
    {
        return \Mtc\Shop\Basket::find($this->id);
    }


    public static function getCurrent()
    {
        $basket = new self();
        $basket->Go_Basket();

        return $basket;
    }

    /**
     * Undocumented function
     *
     * @param int $id delivery method ID
     * @param int $weight shipment weight
     * @return float
     */
    public function estimateDeliveryCost($id, $weight, $default = 0)
    {
        $cost = $default;

        $rate = \Mtc\Shop\DeliveryMethodRate::query()
            ->where('method_id', $id)
            ->where('weight_min', '<=', $weight)
            ->where('weight_max', '>=', $weight)
            ->first();

        if ($rate) {
            // line cost has a fixed price, otherwise we have per kg cost
            if ($rate->line_cost) {
                $cost = $rate->line_cost;
            } else {
                $cost += ceil($weight - $rate->weight_min) * $rate->kg_cost;
            }
        }

        return $cost;
    }

    /**
     * Whether the basket has physical shippable items
     *
     * @return bool
     */
    public function getHasPhysicalItems(): bool
    {
        $itemIDs = [];
        foreach ($this->items as $basketItem) {
            $itemIDs[] = $basketItem['item_id'];
        }
        return \Mtc\Shop\Item::query()
            ->whereIn('id', $itemIDs)
            ->where('product_type', '!=', \Mtc\Shop\Item::TYPE_CONSULTATION)
            ->exists();
    }
}
