<?php

namespace App\Services\Navision;

use App\Jobs\Shopify\SyncCollection;
use App\Jobs\Shopify\SyncPrice;
use App\Jobs\Shopify\SyncProduct;
use App\Models\Catalog;
use App\Models\Product;
use App\Models\ProductCollection;
use App\Models\ProductMeta;
use App\Models\SyncControl;
use App\Models\Variant;
use App\Support\NavClient;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use RuntimeException;

/**
 * Navision API service class
 */
class ProductsService {
    /**
     * Fields used to create the signature that will detect meaningful changes to
     * customers (to avoid unecessary update calls to Shopify)
     */
    const PRODUCT_SIGNATURE_FIELDS = [
        'Description',
        'WebShop_Item',
        'Webshop_Compatible_Software',
        'WebShop_Research_Model',
        'WebShop_Research_Field',
        'WebShop_Product_Name',
        'WebShop_Product_Category',
        'WebShop_Needed_for_BOM',
        'WebShop_Contact_Sales_Item',
        'Assembly_BOM',
        'Item_Disc_Group',
        'Item_Category_Code',
        'Item_Status',
        'Gross_Weight'
    ];

    /**
     * Fields used to create the signature that will detect meaningful changes to
     * resources (to avoid unecessary update calls to Shopify)
     */
    const RESOURCE_SIGNATURE_FIELDS = [
        'WebShop_Item',
        'WebShop_Product_Name',
        'Webshop_Compatible_Software',
        'Unit_Price'
    ];

    /**
     * Maps product metafield data with incoming NAV product fields
     */
    const PRODUCT_META_FIELDS = [
        'research_model' => 'WebShop_Research_Model',
        'research_field' => 'WebShop_Research_Field',
        'compatibility' => 'Webshop_Compatible_Software',
        'is_contact_only' => 'WebShop_Contact_Sales_Item'
    ];
    const NON_SHIPPABLE_CATEGORY_CODES = [
        'NOLDUSSWB',
        'NOLDUSSWM',
        'NOLDUSSWUP',
        'SERVICE',
        'DISCOUNT',
        '3RDPARTYSW'
    ];

    protected $client;

    public function __construct()
    {
        $this->client = new NavClient();
    }

    /**
     * Syncs product from NAV item data
     *
     * @param object $navItem
     * @param bool $forceUpdate - If true ignores last modified date and marks to sync
     * @return Variant
     */
    public function syncItem(object $navItem, object $navPrice, bool $forceUpdate = false)
    {
        DB::beginTransaction();
        try {
            $variant = Variant::where('nav_no', $navItem->No)->first();
            Log::info('Sync Item from NAV: ' . $navItem->No);

            $components = null;
            if ($navItem->Assembly_BOM) {
                $components = $this->_getBundleVariants($navItem->No);
            }

            $productTitle = isset($navItem->WebShop_Product_Name) ? $navItem->WebShop_Product_Name : $navItem->Description;

            if (!$variant) {
                // product still not in database, creates a new one
                $product = new Product();
                $product->nav_no = $navItem->No;
                $product->title = $productTitle;
                $product->save();
                $variant = new Variant();
                $variant->nav_no = $navItem->No;
                $variant->title = 'Default Title';
                $variant->product()->associate($product);
                $variant->save();
            } else {
                $product = $variant->product;
            }

            if ($forceUpdate) {
                $product->sync_hash = '';
            }

            if (!$this->_checkProductUpdate($product, $navItem, $navPrice, $components)) {
                DB::commit();
                Log::info('Item skipped - same hash: ' . $navItem->No . ' - Internal ID: ' . $product->id);
                return $variant;
            }

            $product->title = $productTitle;
            $product->disabled = false;

            if (!empty($navItem->Item_Category_Code)) {
                $product->category_code = $navItem->Item_Category_Code;
                $product->no_shipping = in_array($navItem->Item_Category_Code, self::NON_SHIPPABLE_CATEGORY_CODES);
            }
            if (!empty($navItem->Item_Status)) {
                $product->nav_status = $navItem->Item_Status;
            }

            // Sync Categories
            $currentCollections = $product->collections()->get();
            $toSync = [];

            if ($navItem->WebShop_Item) {
                $isFound = false;
                foreach ($currentCollections as $currentCollection) {
                    if ($currentCollection->nav_id == $navItem->WebShop_Product_Category) {
                        $toSync[] = [
                            'collection_id' => $currentCollection->id,
                            'status' => ($currentCollection->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_REMOVE ? Product::PRODUCT_RELATION_STATUS_SYNCED : $currentCollection->pivot->status)
                        ];
                        $isFound = true;
                    } elseif ($currentCollection->pivot->status != Product::PRODUCT_RELATION_STATUS_TO_ADD) {
                        $toSync[] = [
                            'collection_id' => $currentCollection->id,
                            'status' => Product::PRODUCT_RELATION_STATUS_TO_REMOVE
                        ];
                    }
                }

                if (!$isFound && !empty($navItem->WebShop_Product_Category) && $navItem->WebShop_Product_Category) {
                    $collection = ProductCollection::where('nav_id', $navItem->WebShop_Product_Category)->first();
                    $toSync[] = [
                        'collection_id' => $collection->id,
                        'status' => Product::PRODUCT_RELATION_STATUS_TO_ADD
                    ];
                }
                $product->is_hidden = false;
            } else {
                $product->is_hidden = true;
            }

            $product->collections()->sync($toSync);

            // Sync Discount / Catalogs
            $product->discount_group = $navItem->Item_Disc_Group;
            $toSyncCatalogs = [];
            $currentCatalogs = $product->catalogs()->get();
            $catalogs = Catalog::where('item_discount_group', $product->discount_group)->get();
            $catalogsIds = $catalogs->pluck('id')->toArray();
            foreach ($currentCatalogs as $catalog) {
                if (in_array($catalog->id, $catalogsIds)) {
                    $toSyncCatalogs[] = [
                        'catalog_id' => $catalog->id,
                        'status' => $catalog->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_REMOVE
                            ? Product::PRODUCT_RELATION_STATUS_SYNCED
                            : $catalog->pivot->status
                    ];
                    unset($catalogsIds[array_search($catalog->id, $catalogsIds)]);
                } elseif ($catalog->pivot->status != Product::PRODUCT_RELATION_STATUS_TO_ADD) {
                    $toSyncCatalogs[] = [
                        'catalog_id' => $catalog->id,
                        'status' => Product::PRODUCT_RELATION_STATUS_TO_REMOVE
                    ];
                }
            }

            foreach ($catalogsIds as $catalogId) {
                $toSyncCatalogs[] = [
                    'catalog_id' => $catalogId,
                    'status' => Product::PRODUCT_RELATION_STATUS_TO_ADD
                ];
            }

            $product->catalogs()->sync($toSyncCatalogs);

            foreach (self::PRODUCT_META_FIELDS as $metaFieldKey => $navField) {
                // fallback for boolean types
                if ($metaFieldKey == 'is_contact_only') {
                    $this->_updateProductMeta($product, $metaFieldKey, isset($navItem->$navField) && $navItem->$navField ? $navItem->$navField : '0');
                    continue;
                }
                $this->_updateProductMeta($product, $metaFieldKey, isset($navItem->$navField) ? $navItem->$navField : '');
            }
            $this->_updateProductMeta($product, 'hfp', $product->is_hidden ? '1' : '0');

            $variant->sku = $navItem->No;

            if (isset($navItem->Gross_Weight)) {
                $variant->weight = $navItem->Gross_Weight;
            }

            if ($components) {
                // If bundle doesn't sync variant price
                $product->is_bundle = true;
                $variant->is_bundle = true;
                $variant->to_sync = false;
                $componentsSync = [];
                foreach ($components as $component) {
                    $componentsSync[$component->id] = ['quantity_per_item' => $component->quantity_per_item];
                }
                $variant->componentVariants()->sync($componentsSync);
            } else {
                $product->is_bundle = false;
                $variant->is_bundle = false;
                $this->syncPrice($variant, $navPrice);
            }

            $product->to_sync = 1;
            $product->save();
            $variant->save();
            DB::commit();
            Log::info('Item Updated/Created: ' . $navItem->No . ' - Internal ID: ' . $product->id);
            return $variant;
        } catch (\Throwable $e) {
            Log::error('Error Creating/Updating Item: ' . $navItem->No);
            Log::error($e);
            DB::rollBack();
            return false;
        }
    }

    /**
     * Gets item and price data from Navision, stores and dispatches sync to shopify
     *
     * @param string $itemNo
     * @return void
     */
    public function getAndSyncItem(string $itemNo, bool $forceUpdate = false)
    {
        // get item info
        $navItem = $this->client->getItem($itemNo);
        // get price info
        $navPrice = $this->client->getPrice($itemNo);

        if (!$navItem || !$navPrice) {
            // TODO: deal with error
            return;
        }

        $variant = $this->syncItem($navItem, $navPrice, $forceUpdate);

        if ($variant) {
            SyncProduct::dispatch($variant->product_id);
        }
    }

    /**
     * Gets all non webshop items from NAV to check if the product should be disabled
     * Force update ignores last modified date
     *
     * @param boolean $forceUpdate
     * @return void
     */
    public function checkDisabledItems(bool $forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 200;
        $count = 0;
        $deactivated = 0;
        $syncTime = Carbon::now();
        $filter = [];

        Log::info('Checking webshop disabled items...');

        $filter['WebShop_Item'] = 'false';
        $filter['WebShop_Needed_for_BOM'] = 'false';

        $syncControl = SyncControl::where('type', SyncControl::TYPE_DISABLED_ITEMS)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_DISABLED_ITEMS;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        do {
            $items = $this->client->getItems($filter, $perCall, $bookmark);
            $pageCount = sizeof($items);

            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $items[$pageCount - 1]->Key;

            foreach ($items as $item) {
                if (empty($item->No)) {
                    continue;
                }
                $product = Product::where('nav_no', $item->No)
                    ->where('disabled', 0)
                    ->first();

                if ($product) {
                    $product->disabled = true;
                    $product->to_sync = true;
                    $product->save();
                    SyncProduct::dispatch($product->id);
                    $deactivated++;
                }
            }
        } while (sizeof($items) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Deactivated ' . $deactivated . ' items from Navision.');
    }

    /**
     * Gets all non webshop resources from NAV to check if the product should be disabled
     * Force update ignores last modified date
     *
     * @param boolean $forceUpdate
     * @return void
     */
    public function checkDisabledResources(bool $forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 200;
        $count = 0;
        $deactivated = 0;
        $syncTime = Carbon::now();
        $filter = [];

        Log::info('Checking webshop disabled resources...');

        $filter['WebShop_Item'] = 'false';

        $syncControl = SyncControl::where('type', SyncControl::TYPE_DISABLED_RESOURCES)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_DISABLED_RESOURCES;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        do {
            $resources = $this->client->getResources($filter, $perCall, $bookmark);
            $pageCount = sizeof($resources);

            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $resources[$pageCount - 1]->Key;

            foreach ($resources as $resource) {
                $product = Product::where('nav_no', $resource->No)
                    ->where('item_type', Product::ITEM_TYPE_RESOURCE)
                    ->where('disabled', 0)
                    ->first();

                if ($product) {
                    $product->disabled = true;
                    $product->to_sync = true;
                    $product->save();
                    SyncProduct::dispatch($product->id);
                    $deactivated++;
                }
            }
        } while (sizeof($resources) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Deactivated ' . $deactivated . ' resources from Navision.');
    }

    /**
     * Gets items from NAV and syncs with local data
     * Force update ignores last modified and signature hash check
     * Sync is done separatly for items, bundles and bundle items which are all bundle components
     * that are not supposed to be sold independently
     *
     * @param string $itemType
     * @param boolean $forceUpdate
     * @return Item
     */
    public function getAndSyncItems(string $itemType, bool $forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        $syncTime = Carbon::now();
        $filter = [];

        Log::info('Sync Items (' . $itemType . ') from Navision...');

        switch ($itemType) {
            case SyncControl::TYPE_BUNDLE_ITEMS:
                $filter['WebShop_Item'] = 'false';
                $filter['Assembly_BOM'] = 'false';
                $filter['WebShop_Needed_for_BOM'] = 'true';
                break;
            case SyncControl::TYPE_BUNDLES:
                $filter['WebShop_Item'] = 'true';
                $filter['Assembly_BOM'] = 'true';
                break;
            default:
                $filter['WebShop_Item'] = 'true';
                $filter['Assembly_BOM'] = 'false';
                break;
        }

        $syncControl = SyncControl::where('type', $itemType)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = $itemType;
        }

        /* For now we are not going to control sync with modified date since NAV doesn't handle this well
        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }
        */

        do {
            $items = $this->client->getItems($filter, $perCall, $bookmark);
            $pageCount = sizeof($items);

            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $items[$pageCount - 1]->Key;

            foreach ($items as $item) {
                if (empty($item->No)) {
                    Log::warning('Item with no Number from NAV: ' . json_encode($item));
                    continue;
                }

                $navPrice = $this->client->getPrice($item->No);

                if (!$navPrice) {
                    Log::warning('Item not synced from Nav: No price information for item No. ' . $item->No);
                    continue;
                }
                $variant = $this->syncItem($item, $navPrice, $forceUpdate);

                if (!$forceUpdate && $variant && $variant->product->to_sync) {
                    SyncProduct::dispatch($variant->product_id);
                }
            }
        } while (sizeof($items) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Synced (' . $itemType . ') ' . $count . ' items from Navision.');
    }

    /**
     * Gets resources from NAV and syncs them as regular products locally
     *
     * @param boolean $forceUpdate
     * @return void
     */
    public function getAndSyncResources(bool $forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        $syncTime = Carbon::now();
        $filter = [];

        Log::info('Sync Resources from Navision...');

        $filter['WebShop_Item'] = 'true';

        $syncControl = SyncControl::where('type', SyncControl::TYPE_RESOURCES)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_RESOURCES;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        do {
            $resources = $this->client->getResources($filter, $perCall, $bookmark);
            $pageCount = sizeof($resources);

            if ($pageCount == 0) {
                break;
            }

            $count += $pageCount;
            $bookmark = $resources[$pageCount - 1]->Key;

            foreach ($resources as $resource) {
                $variant = $this->syncResource($resource, $forceUpdate);

                if (!$forceUpdate && $variant && $variant->product->to_sync) {
                    SyncProduct::dispatch($variant->product_id);
                }
            }
        } while (sizeof($resources) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();

        Log::info('Synced ' . $count . ' resource items from Navision.');
    }

    /**
     * Syncs resources from NAV item data
     *
     * @param object $navResource
     * @param bool $forceUpdate - If true ignores last modified date and marks to sync
     * @return Variant
     */
    public function syncResource(object $navResource, bool $forceUpdate = false)
    {
        DB::beginTransaction();
        try {
            $product = Product::where('nav_no', $navResource->No)
                ->where('item_type', Product::ITEM_TYPE_RESOURCE)
                ->first();
            Log::info('Sync Resource from NAV: ' . $navResource->No);

            $resourceTitle = isset($navResource->WebShop_Product_Name) ? $navResource->WebShop_Product_Name : $navResource->Description;

            if (!$product) {
                // product still not in database, creates a new one
                $product = new Product();
                $product->nav_no = $navResource->No;
                $product->item_type = Product::ITEM_TYPE_RESOURCE;
                $product->title = $resourceTitle;
                $product->save();
                $variant = new Variant();
                $variant->nav_no = $navResource->No;
                $variant->title = 'Default Title';
                $variant->product()->associate($product);
                $variant->save();
            } else {
                $variant = $product->variants->first();
            }

            if ($forceUpdate) {
                $product->sync_hash = '';
            }

            if (!$this->_checkResourceUpdate($product, $navResource)) {
                DB::commit();
                Log::info('Item skipped - same hash: ' . $navResource->No . ' - Internal ID: ' . $product->id);
                return $variant;
            }

            $product->title = $resourceTitle;
            $product->disabled = false;
            $product->no_shipping = true;

            foreach (self::PRODUCT_META_FIELDS as $metaFieldKey => $navField) {
                $this->_updateProductMeta($product, $metaFieldKey, isset($navResource->$navField) ? $navResource->$navField : '');
            }
            $this->_updateProductMeta($product, 'hfp', $product->is_hidden ? '1' : '0');

            $variant->sku = $navResource->No;
            $variant->price = $navResource->Unit_Price;
            $product->is_bundle = false;
            $variant->is_bundle = false;
            $product->to_sync = 1;
            $product->save();
            $variant->save();
            DB::commit();
            Log::info('Item (resources) Updated/Created: ' . $navResource->No . ' - Internal ID: ' . $product->id);
            return $variant;
        } catch (\Throwable $e) {
            Log::error('Error Creating/Updating Item (resource): ' . $navResource->No);
            Log::error($e);
            DB::rollBack();
            return false;
        }
    }

    /**
     * Gets and Sync price data for products
     *
     * @param boolean $forceUpdate
     * @return void
     */
    public function getAndSyncPrices(bool $forceUpdate = false)
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        $filter = [
            'Sales_Code' => config('navision.sales_code'),
            'Sales_Type' => config('navision.sales_type')
        ];
        $syncTime = Carbon::now();

        $syncControl = SyncControl::where('type', SyncControl::TYPE_PRICES)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = SyncControl::TYPE_PRICES;
        }

        if (!$forceUpdate && $syncControl->last_sync_at) {
            $filter['Last_Modified_DateTime'] = '>' . $syncControl->last_sync_at->format('m/d/Y H:m:s');
        }

        $foundPrices = 0;
        do {
            $prices = $this->client->getPrices($filter, $perCall, $bookmark);
            $pageCount = sizeof($prices);
            $count += $pageCount;

            if ($pageCount == 0) {
                break;
            }

            $bookmark = $prices[$pageCount - 1]->Key;

            foreach ($prices as $price) {
                $product = Product::where('nav_no', $price->Item_No)
                    ->where('is_bundle', false)
                    ->first();

                if ($product) {
                    $foundPrices++;
                    $variant = $product->variants->first();
                    if ($this->syncPrice($variant, $price)) {
                        if (!$forceUpdate && $variant && $variant->to_sync) {
                            SyncPrice::dispatch($variant->id);
                        }

                        // Marks parent bundles for updating
                        foreach ($variant->parentBundleVariants as $parentVariant) {
                            $parentVariant->product->to_sync = 1;
                            $parentVariant->product->save();

                            if (!$forceUpdate) {
                                SyncProduct::dispatch($parentVariant->product->id);
                            }
                        }
                    }
                }
            }
        } while (sizeof($prices) == $perCall);

        $syncControl->last_sync_at = $syncTime;
        $syncControl->save();
        Log::info('Synced ' . $foundPrices . ' prices from Navision.');
    }

    /**
     * Syncs product variant price from NAV data
     *
     * @param Variant $variant
     * @param object $navPrice
     * @return bool - success
     */
    public function syncPrice(Variant $variant, object $navPrice)
    {
        if ($variant->price == $navPrice->Unit_Price) {
            return false;
        }

        $variant->price = $navPrice->Unit_Price;
        $variant->to_sync = true;
        $variant->save();

        return true;
    }

    /**
     * Gets and syncs NAV options - collection data
     *
     * @return void
     */
    public function getAndSyncOptions()
    {
        $bookmark = null;
        $perCall = 100;
        $count = 0;
        do {
            $options = $this->client->getOptions([
                'Type' => 'Product_Category'
            ], $perCall, $bookmark);
            $pageCount = sizeof($options);
            $count += $pageCount;

            if ($pageCount == 0) {
                break;
            }

            $bookmark = $options[$pageCount - 1]->Key;

            foreach ($options as $option) {
                $this->syncOption($option);
            }
        } while (sizeof($options) == $perCall);
        Log::info('Synced ' . $count . ' from Navision.');
    }

    /**
     * Syncs NAV option to local data
     *
     * @param object $optionData
     * @return void
     */
    public function syncOption(object $optionData)
    {
        if ($optionData->Type == 'Product_Category') {
            $collection = ProductCollection::where('nav_id', $optionData->Code)->first();
            if (!$collection) {
                $collection = new ProductCollection();
                $collection->nav_id = $optionData->Code;
            }
            $collection->title = $optionData->Label;
            $collection->to_sync = $collection->to_sync ||$collection->isDirty('title');
            $collection->save();

            if ($collection->to_sync) {
                SyncCollection::dispatch($collection->id);
            }
        }
    }

    /**
     * Creates or updates product meta for given key
     *
     * @param Product $product
     * @param string $key
     * @param string $value
     * @return ProductMeta
     */
    protected function _updateProductMeta(Product $product, string $key, string $value, string $type = 'single_line_text_field')
    {
        $productMeta = $product->meta()->where('key', $key)->first();
        if (!$productMeta) {
            $productMeta = new ProductMeta();
            $productMeta->key = $key;
            $productMeta->product()->associate($product);
        }
        $productMeta->value = $value == '_blank_' ? '' : $value;
        $productMeta->type = $type;
        $productMeta->save();

        return $productMeta;
    }

    /**
     * Checks and generates a signature to control product data updates
     *
     * @param Product $product
     * @param object $navItem
     * @param object $navPrice
     * @param Collection $bomVariants
     * @return boolean - returns true if product needs updating
     */
    protected function _checkProductUpdate($product, $navItem, $navPrice, $bomVariants = null)
    {
        $signature = [];

        foreach (self::PRODUCT_SIGNATURE_FIELDS as $field) {
            $signature[] = isset($navItem->$field) ? $navItem->$field : '';
        }

        $signature[] = $navPrice->Unit_Price;

        if ($bomVariants) {
            $signature[] = implode(',', $bomVariants->pluck('id')->toArray());
        }

        $currentHash = md5(serialize($signature));
        if ($currentHash === $product->sync_hash) {
            return false;
        }

        $product->sync_hash = $currentHash;
        $product->last_sync_at = Carbon::now();
        $product->to_sync = true;
        return true;
    }

    /**
     * Checks and generates a signature to control product data updates based on resources
     *
     * @param Product $product
     * @param object $navResource
     * @return boolean - returns true if resource needs updating
     */
    protected function _checkResourceUpdate($product, $navResource)
    {
        $signature = [];

        foreach (self::RESOURCE_SIGNATURE_FIELDS as $field) {
            $signature[] = isset($navResource->$field) ? $navResource->$field : '';
        }

        $currentHash = md5(serialize($signature));
        if ($currentHash === $product->sync_hash) {
            return false;
        }

        $product->sync_hash = $currentHash;
        $product->last_sync_at = Carbon::now();
        $product->to_sync = true;
        return true;
    }

    /**
     * Gets bundle component data from NAV
     *
     * @param string $navItemNo - the bundle item nav no
     * @return Collection
     */
    protected function _getBundleVariants(string $navItemNo)
    {
        $maxComponents = config('shopify-app.max_bundle_components');

        $boms = $this->client->getBoms([
            'Parent_Item_No' => $navItemNo
        ], $maxComponents + 1);

        if (sizeof($boms) > $maxComponents) {
            throw new RuntimeException("Bundles with more than $maxComponents components not supported!");
        }

        $componentsNos = array_column($boms, 'No');

        $variants = Variant::whereIn('nav_no', $componentsNos)->get();
        if ($variants->count() != sizeof($boms)) {
            throw new RuntimeException('Missing bundle components. Found ' . $variants->count() . ' of ' . sizeof($boms));
        }

        foreach ($variants as $variant) {
            $found = array_filter($boms, function ($bom) use ($variant) {
                return $variant->nav_no == $bom->No;
            });

            if (empty($found)) {
                throw new RuntimeException('Error matching bom to variant');
            }

            $found = array_pop($found);

            $variant->quantity_per_item = $found->Quantity_per && is_numeric($found->Quantity_per) ? $found->Quantity_per * 1 : 1;
        }

        return $variants;
    }
}
