<?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\Incompatibility;
use Mtc\Shop\Item as ItemModel;
use Mtc\Shop\Item\Size;
use Mtc\Shop\PlaceholderImage;
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 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;

    /**
     * Cached basket data for eager loading.
     *
     * @var array<int, \Mtc\Shop\Item\Size>
     */
    protected array $basketSizeCache = [];

    /**
     * @var array<int, \Mtc\Plugins\NHS\Classes\PrescriptionItem>
     */
    protected array $basketPrescriptionCache = [];

    /**
     * @var array<int, \MtcPharmacy\Bundles\Classes\Bundle>
     */
    protected array $basketBundleCache = [];

    /**
     * @var array<int, ItemModel>
     */
    protected array $basketShopItems = [];

    /**
     * @var array<int, int[]>
     */
    protected array $itemCategoriesMap = [];

    /**
     * @var array<int, int[]>
     */
    protected array $categoryRestrictedZonesMap = [];

    /**
     * @var array<int, int[]>
     */
    protected array $itemDeliveryMethodMap = [];

    /**
     * @var array<int, int[]>
     */
    protected array $itemIngredientIdsMap = [];

    /**
     * @var array<int, array<int, array{message:string,categories:int[]}>>
     */
    protected array $categoryIncompatibilityMap = [];

    /**
     * @var array<string, SubscriptionPreference>
     */
    protected array $subscriptionPreferenceMap = [];

    /**
     * @var array<int, array<int, array{weight_min:float,weight_max:float,line_cost:float,kg_cost:float}>>
     */
    protected array $deliveryMethodRates = [];

    protected bool $shouldValidateIngredientLimits = false;
    protected bool $shouldValidateIncompatibilities = false;

    /**
     * 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;

    /**
     * Additional params
     */
    public $date;
    public $delivery;
    public $timestamp;
    public $checkout;
    public $seccode;
    
    /**
     * Preload basket item relations to avoid N+1 queries.
     */
    private function preloadBasketItemData(Collection $basketItems): void
    {
        $this->basketSizeCache = [];
        $this->basketPrescriptionCache = [];
        $this->basketBundleCache = [];
        $this->basketShopItems = [];
        $this->itemCategoriesMap = [];
        $this->categoryRestrictedZonesMap = [];
        $this->itemDeliveryMethodMap = [];
        $this->itemIngredientIdsMap = [];
        $this->categoryIncompatibilityMap = [];
        $this->subscriptionPreferenceMap = [];
        $this->deliveryMethodRates = [];
        $this->shouldValidateIngredientLimits = false;
        $this->shouldValidateIncompatibilities = false;

        if ($basketItems->isEmpty()) {
            return;
        }

        $itemIds = $basketItems->pluck('item_id')->filter()->unique()->values()->all();
        $sizeIds = $basketItems->pluck('sizeid')->filter()->unique()->values()->all();
        $prescriptionIds = $basketItems->pluck('prescription_item_id')->filter()->unique()->values()->all();
        $bundleIds = $basketItems->pluck('bundle_id')->filter()->unique()->values()->all();

        if (!empty($sizeIds)) {
            $this->basketSizeCache = Size::query()
                ->whereIn('id', $sizeIds)
                ->get()
                ->keyBy('id')
                ->all();
        }

        if (!empty($prescriptionIds)) {
            $this->basketPrescriptionCache = PrescriptionItem::query()
                ->whereIn('id', $prescriptionIds)
                ->get()
                ->keyBy('id')
                ->all();
        }

        if (!empty($bundleIds)) {
            $this->basketBundleCache = Bundle::query()
                ->with('type')
                ->whereIn('id', $bundleIds)
                ->get()
                ->keyBy('id')
                ->all();
        }

        if (!empty($itemIds)) {
            $subscriptionSizeIds = array_unique(array_merge($sizeIds, [0]));
            $preferences = SubscriptionPreference::query()
                ->whereIn('item_id', $itemIds)
                ->whereIn('item_size_id', $subscriptionSizeIds)
                ->get();

            foreach ($preferences as $preference) {
                $this->subscriptionPreferenceMap[$preference->item_id . ':' . (int)($preference->item_size_id ?? 0)] = $preference;
            }
        }

        if (!empty($itemIds)) {
            $this->basketShopItems = ItemModel::query()
                ->whereIn('id', $itemIds)
                ->with([
                    'defaultImage',
                    'custom',
                    'categories.restricted_zones',
                    'deliveryMethods',
                    'ingredients',
                    'brands',
                    'groupbuy_bundletype',
                ])
                ->get()
                ->keyBy('id')
                ->all();

            $categoryIdsForIncompatibility = [];

            foreach ($this->basketShopItems as $itemModel) {
                $categoryIds = $itemModel->categories->pluck('id')->map('intval')->all();
                $this->itemCategoriesMap[$itemModel->id] = $categoryIds;
                $categoryIdsForIncompatibility = array_merge($categoryIdsForIncompatibility, $categoryIds);

                foreach ($itemModel->categories as $category) {
                    if ($category->restricted_zones->isNotEmpty()) {
                        $zones = $category->restricted_zones->pluck('zone')->map('intval')->all();
                        if (!empty($zones)) {
                            $this->categoryRestrictedZonesMap[$category->id] = array_values(array_unique($zones));
                        }
                    }
                }

                if ($itemModel->deliveryMethods->isNotEmpty()) {
                    $this->itemDeliveryMethodMap[$itemModel->id] = $itemModel->deliveryMethods->pluck('id')->map('intval')->all();
                }

                if ($itemModel->ingredients->isNotEmpty()) {
                    $this->itemIngredientIdsMap[$itemModel->id] = $itemModel->ingredients->pluck('id')->map('intval')->all();
                    $this->shouldValidateIngredientLimits = true;
                }

                if (
                    (int)$itemModel->restriction_period_length > 0 ||
                    (int)$itemModel->restriction_per_period > 0 ||
                    (int)$itemModel->restriction_per_order > 0 ||
                    (int)$itemModel->restriction_limit_once > 0
                ) {
                    $this->shouldValidateIngredientLimits = true;
                }
            }

            $categoryIdsForIncompatibility = array_values(array_unique($categoryIdsForIncompatibility));

            if (count($categoryIdsForIncompatibility) > 1) {
                $incompatibilities = Incompatibility::query()
                    ->select(['id', 'message'])
                    ->whereHas('categories', function ($query) use ($categoryIdsForIncompatibility): void {
                        $query->whereIn('categories.id', $categoryIdsForIncompatibility);
                    })
                    ->with(['categories:id'])
                    ->get();

                foreach ($incompatibilities as $incompatibility) {
                    $catIds = $incompatibility->categories->pluck('id')->map('intval')->all();

                    if (count($catIds) < 2) {
                        continue;
                    }

                    foreach ($catIds as $categoryId) {
                        $otherCategories = array_values(array_diff($catIds, [$categoryId]));
                        if (empty($otherCategories)) {
                            continue;
                        }

                        $this->categoryIncompatibilityMap[$categoryId][] = [
                            'message' => $incompatibility->message,
                            'categories' => $otherCategories,
                        ];
                    }
                }

                $this->shouldValidateIncompatibilities = !empty($this->categoryIncompatibilityMap);
            }
        }
    }

    /**
     * Resolve basket image URL and alt text.
     *
     * @return array{0:string,1:string}
     */
    private function resolveBasketImage(ItemModel $itemModel): array
    {
        static $thumbPaths = null;
        static $placeholderImages = null;

        if ($thumbPaths === null) {
            $image_folders = [];
            require SITE_PATH . '/shop/includes/image_folders.php';
            $thumbPath = $image_folders['product_folders']['thumbs']['path'] ?? '';
            $thumbPaths = [
                'url' => '/' . ltrim($thumbPath, '/') . '/',
                'path' => rtrim(SITE_PATH, '/') . '/' . trim($thumbPath, '/\\') . '/',
            ];
        }

        if ($placeholderImages === null) {
            $placeholderImages = PlaceholderImage::getPackedData();
        }

        $defaultImage = $itemModel->defaultImage;
        if ($defaultImage && !empty($defaultImage->name)) {
            $filePath = $thumbPaths['path'] . $defaultImage->name;
            if (is_file($filePath)) {
                $alt = $defaultImage->alt ?: $itemModel->name;
                return [$thumbPaths['url'] . $defaultImage->name, $alt];
            }
        }

        if (!empty($placeholderImages['item_small']['value'])) {
            return [$thumbPaths['url'] . $placeholderImages['item_small']['value'], $itemModel->name];
        }

        return ['', $itemModel->name];
    }

    /**
     * Build basket category payload from loaded item model.
     *
     * @return array<int, array<string, mixed>>
     */
    private function buildItemCategories(ItemModel $itemModel): array
    {
        return $itemModel->categories->pluck('id')->map('intval')->all();
    }

    private function basketContainsCategory(array $categoryIds, int $excludingBasketItemId): bool
    {
        if (empty($categoryIds)) {
            return false;
        }

        foreach ($this->items as $item) {
            if ((int)($item['id'] ?? 0) === $excludingBasketItemId) {
                continue;
            }
            $itemCategories = array_map('intval', $item['categories'] ?? []);
            if (!empty(array_intersect($itemCategories, $categoryIds))) {
                return true;
            }
        }

        return false;
    }

    /**
     * @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'];
            }
            // Save basket on member if it's not been saved yet
            if (
                !empty($member->id) &&
                empty($member->basket_id) &&
                !empty($this->id)
            ) {
                $member->basket_id = $this->id;
                $member->save();
            }

            // 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['dob'] = '';
        $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)) {
                if (!empty($member->dob)) {
                    $this->info['dob'] = $member->dob;
                    $dobDate = Carbon::parse($member->dob);
                    $this->info['dob_date'] = $dobDate->format('d');
                    $this->info['dob_month'] = $dobDate->format('m');
                    $this->info['dob_year'] = $dobDate->format('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;
            }
            if ($key === 'delivery') {
                $this->delivery_selected = $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',
                'firstname' => null,
                'middle_name' => null,
                'lastname' => null,
                'address1' => null,
                'address2' => null,
                'city' => null,
                'postcode' => null,
            ];
            $this->setCustomerAddress('billing', $default);
            // Set users info fields as empty array as no data available
            $this->setCustomerInfo([
                'email' => null,
                'contact_no' => null,
                'dob' => null,
            ]);
        }

        $_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

        if ($member->addressBilling) {
            $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,
            'dob' => $member->dob,
        ]);

        // 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,
                ]);

                $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
    {
        $this->items = [];
        $this->num_items = 0;
        $this->cost_subtotal = 0;
        $this->cost_subtotal_exvat = 0;
        $this->cost_vat = 0;
        $this->vat_deductable_amount = 0;
        $this->weight = 0;
        $this->delivery_heavy = 0;

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

        $basketItems = \Mtc\Shop\Basket\Item::query()
            ->where('basket_id', $this->id)
            ->get();

        if ($basketItems->isEmpty()) {
            return [];
        }

        $this->preloadBasketItemData($basketItems);

        $conversionCurrency = $_SESSION['currency']['currency'] ?? null;

        foreach ($basketItems as $basketItem) {
            $itemId = (int)$basketItem->item_id;
            $itemModel = $this->basketShopItems[$itemId] ?? null;

            if (!$itemModel) {
                continue;
            }

            $data = $basketItem->toArray();
            $data['id'] = (int)($data['id'] ?? 0);
            $data['item_id'] = $itemId;
            $data['quantity'] = (int)($data['quantity'] ?? 0);
            $data['recommended_quantity'] = (int)($data['recommended_quantity'] ?? 1);
            $data['sizeid'] = (int)($data['sizeid'] ?? 0);
            $data['bundle_id'] = (int)($data['bundle_id'] ?? 0);
            $data['prescription_item_id'] = (int)($data['prescription_item_id'] ?? 0);
            $data['nhs_prescription'] = (bool)($data['nhs_prescription'] ?? false);
            $data['has_requested_subscription'] = (bool)($data['has_requested_subscription'] ?? false);

            [$imageUrl, $imageAlt] = $this->resolveBasketImage($itemModel);
            $data['item_image'] = $imageUrl;
            $data['item_image_alt'] = $imageAlt;
            $data['item_name'] = $itemModel->name;
            $data['item_url'] = $itemModel->url ?? '';
            $data['preorder'] = (bool)($itemModel->custom->preorder ?? false);

            $price = (float)$itemModel->price;
            $priceExVat = (float)$itemModel->price_exvat;
            $originalPrice = $price;

            if ((float)$itemModel->sale_price > 0) {
                $price = (float)$itemModel->sale_price;
                $priceExVat = (float)$itemModel->sale_price_exvat;
            }

            if (defined('ITEMS_SIZES_PRICES') && ITEMS_SIZES_PRICES && $data['sizeid'] > 0 && isset($this->basketSizeCache[$data['sizeid']])) {
                /** @var Size $size */
                $size = $this->basketSizeCache[$data['sizeid']];

                if ((float)$size->price > 0) {
                    $price = (float)$size->price;
                    $priceExVat = (float)$size->price_exvat;
                    $originalPrice = (float)$size->price;
                }

                if ((float)$size->sale_price > 0) {
                    $price = (float)$size->sale_price;
                    $priceExVat = (float)$size->sale_price_exvat;
                }
            }

            $data['original_price'] = $originalPrice;
            $data['item_price'] = $price;
            $data['item_price_exvat'] = $priceExVat;

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

            if ($itemModel->heavy) {
                $this->delivery_heavy = 1;
            }

            $data['item_heavy'] = (bool)$itemModel->heavy;
            $data['vat_deductable'] = (bool)$itemModel->vat_deductable;
            $data['vat_rate'] = (float)$itemModel->vat_rate;
            $data['quantity_lock'] = false;

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

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

            if ($data['prescription_item_id'] > 0 && isset($this->basketPrescriptionCache[$data['prescription_item_id']])) {
                $prescriptionItem = $this->basketPrescriptionCache[$data['prescription_item_id']];
                $data['nhs_prescription_item'] = $prescriptionItem->toArray();
                $data['item_name'] = 'NHS Prescription Item // ' . $prescriptionItem->name;
                $data['item_price'] = $prescriptionItem->get_price();
                $data['item_price_exvat'] = $data['item_price'];
                $data['original_price'] = $prescriptionItem->get_price();
                $data['quantity'] = (int)$prescriptionItem->quantity;
            }

            if ($data['bundle_id'] > 0 && isset($this->basketBundleCache[$data['bundle_id']])) {
                $bundle = $this->basketBundleCache[$data['bundle_id']];
                if ($bundle && $bundle->type) {
                    $data['item_name'] .= " ({$bundle->type->name} bundle)";
                    $data['item_url'] = "{$bundle->type->url}?bundle_id={$bundle->id}";
                }
            }

            $baseItemPrice = $data['item_price'];
            $baseItemPriceExVat = $data['item_price_exvat'];
            $extraLineTotal = 0.0;
            $extraLineTotalExvat = 0.0;

            $data['additional'] = [];
            $additionResponses = Event::dispatch(new BasketGetItemsEvent($data, $this));
            if (!empty($additionResponses)) {
                foreach ($additionResponses as $additionArray) {
                    if (is_array($additionArray)) {
                        $data['additional'] = array_merge($data['additional'], $additionArray);
                    }
                }
            }

            foreach ($data['additional'] as $addition) {
                if (!empty($addition['item_price'])) {
                    $baseItemPrice += (float)$addition['item_price'];
                }
                if (!empty($addition['item_price_exvat'])) {
                    $baseItemPriceExVat += (float)$addition['item_price_exvat'];
                }
                if (!empty($addition['line_total'])) {
                    $extraLineTotal += (float)$addition['line_total'];
                }
                if (!empty($addition['line_total_exvat'])) {
                    $extraLineTotalExVat += (float)$addition['line_total_exvat'];
                }
                if (!empty($addition['cost_subtotal'])) {
                    $this->cost_subtotal += (float)$addition['cost_subtotal'];
                }
                if (!empty($addition['cost_subtotal_exvat'])) {
                    $this->cost_subtotal_exvat += (float)$addition['cost_subtotal_exvat'];
                }
                if (!empty($addition['quantity_lock'])) {
                    $data['quantity_lock'] = true;
                }
            }

            $data['item_price'] = $baseItemPrice;
            $data['item_price_exvat'] = $baseItemPriceExVat;

            $data['line_total'] = ($baseItemPrice * $data['quantity']) + $extraLineTotal;
            $data['line_total_exvat'] = ($baseItemPriceExVat * $data['quantity']) + $extraLineTotalExvat;
            $data['afterdiscount_item_price'] = $data['item_price'];
            $data['afterdiscount_item_price_exvat'] = $data['item_price_exvat'];
            $data['afterdiscount_line_total'] = $data['line_total'];
            $data['afterdiscount_line_total_exvat'] = $data['line_total_exvat'];
            $data['original_total'] = $data['original_price'] * $data['quantity'];
            $data['price_paid'] = $data['item_price'];
            $data['price_paid_exvat'] = $data['item_price_exvat'];

            if ($conversionCurrency) {
                $conversionRate = $this->c_rates[$conversionCurrency] ?? 1;
                $data['price_converted'] = $data['item_price'] * $conversionRate;
            }

            $data['categories'] = $this->buildItemCategories($itemModel);
            $data['category_ids'] = $this->itemCategoriesMap[$itemId] ?? [];
            $data['brand'] = optional($itemModel->brands->first())->id;
            $data['product_type'] = $itemModel->product_type;
            $data['can_remove'] = true;

            $vatContribution = $data['line_total'] - $data['line_total_exvat'];
            if ($data['vat_deductable']) {
                $this->vat_deductable_amount += $vatContribution;
            }

            $this->cost_subtotal += $data['line_total'];
            $this->cost_subtotal_exvat += $data['line_total_exvat'];
            $this->cost_vat += $vatContribution;
            $this->num_items += $data['quantity'];

            $data['manage_subscription_button'] = ['url' => '', 'label' => ''];
            $subscriptionKey = $itemId . ':' . $data['sizeid'];
            $sub_config = $this->subscriptionPreferenceMap[$subscriptionKey] ?? null;
            if (!$sub_config && $data['sizeid'] !== 0) {
                $sub_config = $this->subscriptionPreferenceMap[$itemId . ':0'] ?? null;
            }

            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 ($itemModel->is_groupbuy_container && $itemModel->groupbuy_bundletype) {
                $includedShopItems = $itemModel->groupbuy_bundletype->getShopItems();
                if ($includedShopItems) {
                    $data['included_groupbuy_items'] = collect($includedShopItems)->map(function ($included_item) {
                        return [
                            'id' => $included_item->id,
                            'url' => $included_item->url,
                            'name' => $included_item->name,
                        ];
                    })->toArray();
                }
            }

            $this->items[] = $data;
        }

        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 (count($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 = $this->hasDoctorItems() ? 1 : 0;

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

        $restrictedItemDeliveryMaps = array_filter($this->itemDeliveryMethodMap, function ($deliveryMethods) {
            return !empty($deliveryMethods);
        });

        $deliveryMethods = DeliveryMethod::query()
            ->where('disable', 0)
            ->where('heavy', $this->delivery_heavy)
            ->where('zone', $this->zone)
            ->orderBy('order', 'asc')
            ->orderBy('cost', 'asc')
            ->get();

        $methodIds = $deliveryMethods->pluck('id')->all();
        $deliveryMethodRates = [];
        if (!empty($methodIds)) {
            $deliveryMethodRates = \Mtc\Shop\DeliveryMethodRate::query()
                ->whereIn('method_id', $methodIds)
                ->orderBy('weight_min')
                ->get()
                ->groupBy('method_id')
                ->map(function ($group) {
                    return $group->map(function ($rate) {
                        return [
                            'weight_min' => (float)$rate->weight_min,
                            'weight_max' => (float)$rate->weight_max,
                            'line_cost' => (float)$rate->line_cost,
                            'kg_cost' => (float)$rate->kg_cost,
                        ];
                    })->all();
                })
                ->all();
        }
        $this->deliveryMethodRates = $deliveryMethodRates;

        $this->delivery_options = $deliveryMethods
            ->reject(function ($method) use ($weight, $has_doctor_items, $restrictedItemDeliveryMaps, $deliveryMethodRates) {

                if ($method->zone === 3 && $has_doctor_items) {
                    return true;
                }

                foreach ($restrictedItemDeliveryMaps as $allowedDeliveryMethodIds) {
                    if (!in_array($method->id, $allowedDeliveryMethodIds, true)) {
                        return true;
                    }
                }

                $rates = $deliveryMethodRates[$method->id] ?? [];
                $rate_exists = false;
                foreach ($rates as $rate) {
                    if ($weight >= $rate['weight_min'] && $weight <= $rate['weight_max']) {
                        $rate_exists = true;
                        break;
                    }
                }

                if (!$rate_exists) {
                    foreach ($rates as $rate) {
                        if ($rate['weight_max'] < $weight) {
                            return true;
                        }
                    }

                    return false;
                }

                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
        $rateMatch = null;
        $rates = $this->deliveryMethodRates[$selected['id']] ?? [];
        foreach ($rates as $rate) {
            if ($this->weight >= $rate['weight_min'] && $this->weight <= $rate['weight_max']) {
                $rateMatch = $rate;
                break;
            }
        }

        if ($rateMatch) {
            if ($rateMatch['line_cost']) {
                $this->cost_delivery = $rateMatch['line_cost'];
            } else {
                $this->cost_delivery += ceil($this->weight - $rateMatch['weight_min']) * $rateMatch['kg_cost'];
            }
        } elseif (empty($rates)) {
            $rate = \Mtc\Shop\DeliveryMethodRate::query()
                ->where('method_id', $selected['id'])
                ->where('weight_min', '<=', $this->weight)
                ->where('weight_max', '>=', $this->weight)
                ->first();

            if ($rate) {
                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 += $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) {
            $stock = 0;
            if (!empty($basketItem['sizeid']) && $basketItem['sizeid'] > 0 && isset($this->basketSizeCache[$basketItem['sizeid']])) {
                /** @var Size $size */
                $size = $this->basketSizeCache[$basketItem['sizeid']];
                $stock = (int)($size->stock ?? 0);
            } elseif (isset($this->basketShopItems[$basketItem['item_id']])) {
                $stock = (int)($this->basketShopItems[$basketItem['item_id']]->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 (isset($request['dob_date']) && isset($request['dob_month']) && isset($request['dob_year'])) {
            $request['dob'] = $request['dob_year'] . '-' . $request['dob_month'] . '-' . $request['dob_date'];
            unset($request['dob_date']);
            unset($request['dob_month']);
            unset($request['dob_year']);
        }

        if (empty($request['id'])) {
            $request['basket_id'] = $this->id;
            $info = new BasketInfo();
            $info->fill($request);
            $info->save();
            $request['id'] = $info->id;
        } elseif ($basketInfo = BasketInfo::query()->find($request['id'])) {
            $basketInfo->fill($request);
            $basketInfo->save();
        }
        $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
    {
        // Guard: Don't save to database if basket doesn't exist yet
        if (empty($this->id)) {
            $this->address[$type] = $request;
            $this->address[$type]['type'] = $type;
            return;
        }

        //clean data
        foreach ($request as $key => $value) {
            $request[$key] = strip_tags($value);
        }

        $request['type'] = $type;
        $request['basket_id'] = $this->id;

        // Use firstOrNew to find existing address by basket_id and type, preventing duplicates
        // This ensures only one address record exists per basket/type combination
        $address = Address::query()->firstOrNew([
            'basket_id' => $this->id,
            'type' => $type
        ]);
        $address->fill($request);
        $address->basket_id = $this->id;  // Explicitly set - not in $fillable
        $address->save();

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

    /**
     * Basket::Add_CustomerAddress().
     *
     * @return void
     */
    public function Add_CustomerAddress(): void
    {
        // Guard: Don't save to database if basket doesn't exist yet
        if (empty($this->id)) {
            return;
        }

        foreach ($this->address as $type => $data) {
            if (empty($data['id'])) {
                $data['type'] = $type;
                $data['basket_id'] = $this->id;

                // Use firstOrNew to prevent duplicates - check if address already exists
                $address = Address::query()->firstOrNew([
                    'basket_id' => $this->id,
                    'type' => $type
                ]);
                $address->fill($data);
                $address->basket_id = $this->id;  // Explicitly set - not in $fillable
                $address->save();

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


    /**
     * 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 (count($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 (count($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 += $discount['discount_amount'];
        }

        $this->cost_subtotal_beforediscounts = $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';
        }

        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;
    }

    /**
     * Checks that the correct member details are being updated
     *
     * @return array
     */
    public function validateMemberSession($memberID): array
    {
        if (empty($memberID)) {
            return [];
        }
        $member = Auth::getLoggedInMember();
        if ((int)$member->id !== (int)$memberID) {
            return [
                'info' => [
                    'email' => 'You are attempting to update the account with another accounts details. Please refresh the page to ensure you are seeing the correct details.'
                ],
            ];
        }
        return [];
    }

    /**
     * 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(array $required): array
    {
        $errors = [];

        foreach ($required['info'] as $required_field) {
            if (empty($this->info[$required_field])) {
                $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 (!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 (count($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 (empty($this->categoryRestrictedZonesMap) || count($this->items) === 0) {
            return true;
        }

        foreach ($this->items as $key => $basket_item) {
            unset($this->items[$key]['delivery_error']);

            $categoryIds = $this->itemCategoriesMap[$basket_item['item_id']] ?? array_map('intval', $basket_item['categories'] ?? []);
            $restricted = false;

            foreach ($categoryIds as $categoryId) {
                $zones = $this->categoryRestrictedZonesMap[$categoryId] ?? [];
                if (in_array((int)$delivery_method->zone, $zones, true)) {
                    $restricted = true;
                    break;
                }
            }

            if ($restricted) {
                $message = '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->delivery_error[$basket_item['id']] = $message;
                $this->items[$key]['delivery_error'] = $message;
            }
        }

        return empty($this->delivery_error);
    }

    /**
     * Check if basket quantity does not exceed purchase limits set for products
     * @return bool
     */
    public function validateIngredientLimits()
    {
        if (!$this->shouldValidateIngredientLimits) {
            return true;
        }

        // 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): void {
                $query->whereIn('items.id', $basket_item_ids);
            })
            ->get()
            ->each(function (Ingredient $ingredient) use ($basket, $ingredient_order_history): void {

                // 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): void {
                $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 (!$this->shouldValidateIncompatibilities || count($this->items) < 2) {
            return true;
        }

        $incompatibilitiesAdded = [];

        foreach ($this->items as $basket_item) {
            $basketItemId = (int)($basket_item['id'] ?? 0);
            $categoryIds = array_map('intval', $basket_item['categories'] ?? []);

            foreach ($categoryIds as $categoryId) {
                $incompatibilities = $this->categoryIncompatibilityMap[$categoryId] ?? [];
                if (empty($incompatibilities)) {
                    continue;
                }

                foreach ($incompatibilities as $incompatibility) {
                    $conflictKey = $basketItemId . '|' . $categoryId . '|' . $incompatibility['message'];
                    if (in_array($conflictKey, $incompatibilitiesAdded, true)) {
                        continue;
                    }

                    if (!$this->basketContainsCategory($incompatibility['categories'], $basketItemId)) {
                        continue;
                    }

                    $this->setIncompatibilityError(
                        [
                            $basket_item['id'],
                        ],
                        $incompatibility['message']
                    );
                    $incompatibilitiesAdded[] = $conflictKey;
                }
            }
        }

        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;
    }

    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(bool $loadFull = true)
    {
        $basket = new self();
        if ($loadFull) {
            $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 = null;
        if (!empty($this->deliveryMethodRates[$id])) {
            foreach ($this->deliveryMethodRates[$id] as $candidate) {
                if ($weight >= $candidate['weight_min'] && $weight <= $candidate['weight_max']) {
                    $rate = $candidate;
                    break;
                }
            }
        }

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

            if ($dbRate) {
                $rate = [
                    'line_cost' => (float)$dbRate->line_cost,
                    'kg_cost' => (float)$dbRate->kg_cost,
                    'weight_min' => (float)$dbRate->weight_min,
                ];
            }
        }

        if ($rate) {
            if (!empty($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
    {
        if (!empty($this->basketShopItems)) {
            foreach ($this->basketShopItems as $itemModel) {
                if ($itemModel->product_type !== ItemModel::TYPE_CONSULTATION) {
                    return true;
                }
            }
            return false;
        }

        $itemIDs = [];
        foreach ($this->items as $basketItem) {
            $itemIDs[] = $basketItem['item_id'];
        }
        if (empty($itemIDs)) {
            return false;
        }

        return \Mtc\Shop\Item::query()
            ->whereIn('id', $itemIDs)
            ->where('product_type', '!=', \Mtc\Shop\Item::TYPE_CONSULTATION)
            ->exists();
    }

    public function getCachedItemModel(int $itemId): ?ItemModel
    {
        return $this->basketShopItems[$itemId] ?? null;
    }
}
