<?php

namespace App\Shopify;

use App\Helpers\StringHelper;
use App\Models\Address;
use App\Models\Category;
use App\Models\Customer;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Price;
use App\Models\Product;
use App\Models\User;
use Carbon\Carbon;
use RuntimeException;
use stdClass;
use Illuminate\Support\Facades\DB;

/**
 * Service class that holds all Shopify related interaction and business logic
 */
class ShopifyService
{
    const PRODUCT_SYNC_CHUNK_SIZE = 500;
    const PRICE_SYNC_CHUNK_SIZE = 500;

    const BSTAT_TAG_MAP = [
        "A" => "creation-status-active",
        "#" => "creation-status-next-season",
        "L" => "creation-status-limited",
        "*" => "creation-status-discontinued",
        "@" => "creation-status-client"
    ];

    protected $shop;
    protected $api;

    public function __construct()
    {
        $shopifyStore = config('shopify-app.sync_store');
        $this->shop = User::where('name', $shopifyStore)->first();
        if (!$this->shop) {
            throw new RuntimeException('Shopify store not found! Perhaps not installed yet? - ' . $shopifyStore);
        }
        $this->api = $this->shop->api();
        $this->langs = config('shopify-app.available_languages');
        $this->defaultLang = config('shopify-app.default_language');
    }

    /**
     * Gets Shopify product metafields
     *
     * @param integer $shopifyId
     * @return array - Shopify product data with metafields
     */
    protected function getProductMetafields(int $shopifyId)
    {
        $query = '{product(id: "gid://shopify/Product/' . $shopifyId . '"){id, metafields(first:40){edges{node {id, key, value}}}}}';

        $response = $this->api->graph($query);
        return $response['body']['data']['product'];
    }

    /**
     * Syncs marked product price info with shopify (PRICE_SYNC_CHUNK_SIZE at a time)
     *
     * @return void
     */
    public function syncPrices()
    {
        // Processes parent products prices (for product tagging)
        $parentPrices = Price::select('prices.*')
            ->with(['product'])
            ->where('prices.to_sync', true)
            ->where('products.is_parent', true)
            ->join('products', 'products.id', 'prices.product_id')
            ->orderBy('prices.last_sync_at', 'ASC')
            ->get();

        foreach ($parentPrices as $price) {
            $this->syncParentPrice($price);
        }

        // Processes product / variant prices
        $prices = Price::select('prices.*')
            ->with(['product'])
            ->where('prices.to_sync', true)
            ->where('products.is_parent', false)
            // ->whereNotNull('products.variant_id')
            ->where(function ($query) {
                $query->whereNotNull('products.variant_id')
                    ->orWhereNotNull('products.child_variant_id');
            })
            ->join('products', 'products.id', 'prices.product_id')
            ->limit(self::PRICE_SYNC_CHUNK_SIZE)
            ->orderBy('prices.last_sync_at', 'ASC')
            ->get();

        foreach ($prices as $price) {
            $this->syncPrice($price);
        }
    }

    /**
     * Syncs parent products price (just tags since price and inventory info belongs to child products)
     *
     * @param Price $price
     * @return void
     */
    protected function syncParentPrice(Price $price)
    {
        $this->updateProductTags($price->product);
        $price->to_sync = false;
        $price->save();
    }

    /**
     * Syncs products that are marked to be disabled in Shopify.
     * Child products delete the variant in shopify and then delete themselves
     * Single and parent products are just archived in shopify and marked as disabled
     *
     * @return void
     */
    public function syncDisabledProducts()
    {
        $products = Product::where('to_disable', true)
            ->get();
        \Log::info('Found ' . $products->count() .  ' products to archive in Shopify...');
        foreach ($products as $product) {
            if ($product->child_variant_id) {
                if (!$product->parent || !$product->parent->shopify_id) {
                    \Log::error('Child product has no parent: ' . $product->sap_matnr);
                    continue;
                }
                $parentProductGid = 'gid://shopify/Product/' . $product->parent->shopify_id;
                \Log::warning("Deleting Product Variant corresponding to " . $product->sap_matnr . "...");
                $query = 'mutation bulkDeleteProductVariants($productId: ID!, $variantsIds: [ID!]!) {
                        productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {
                            product {
                                id
                            }
                            userErrors {
                                field
                                message
                            }
                        }
                    }
                ';

                $response = $this->api->graph($query, [
                    'productId' => $parentProductGid,
                    'variantsIds' => ['gid://shopify/ProductVariant/' . $product->child_variant_id]
                ]);

                if (($response['status'] !== 200 && $response['status'] !== 201) || !empty($response['errors'])) {
                    \Log::error('Unable to delete product variant for product id (' . $product->sap_matnr . '): ' .  serialize($response['body']));
                    continue;
                }
                $product->child_variant_id = null;
                $product->child_inventory_item_id = null;
                \Log::info('Shopify child variant deleted: ' . $product->sap_matnr);
            }

            if ($product->shopify_id) {
                if (!$this->archiveProduct($product)) {
                    return false;
                }
                \Log::info('Shopify product archived for: ' . $product->sap_matnr);
            }

            $product->disabled = true;
            $product->to_disable = false;
            $product->to_sync = false;
            $product->save();
        }
    }

    /**
     * Archives a given Product in Shopify
     *
     * @param Product $product
     * @return bool - Success status
     */
    protected function archiveProduct(Product $product)
    {
        $query = '
                mutation productUpdate($input: ProductInput!) {
                    productUpdate(input: $input) {
                        product {
                            id
                        }
                        userErrors {
                            field
                            message
                        }
                    }
                }
            ';

        $collectionsToLeave = [];
        if (!empty($product->category->shopify_id)) {
            $collectionsToLeave[] = 'gid://shopify/Collection/' . $product->category->shopify_id;
        }

        $response = $this->api->graph($query, [
            'input' => [
                'id' => 'gid://shopify/Product/' . $product->shopify_id,
                'status' => 'ARCHIVED',
                'tags' => '',
                'collectionsToLeave' => $collectionsToLeave
            ]
        ]);

        if (($response['status'] !== 200 && $response['status'] !== 201) || !empty($response['errors'])) {
            \Log::error('Unable to archive product id (' . $product->sap_matnr . '): ' .  serialize($response));
            return false;
        }
        return true;
    }

    /**
     * Updates pending products (marked to sync) in Shopify.
     * PRODUCT_SYNC_CHUNK_SIZE at a time
     *
     * @return void
     */
    public function syncProducts()
    {
        \Log::info('Sync Products to Shopify...');
        $this->syncDisabledProducts();
        $products = Product::with('price')
            ->where('to_sync', true)
            ->where('disabled', false)
            ->whereNull('parent_id')
            ->limit(self::PRODUCT_SYNC_CHUNK_SIZE)
            ->orderBy('last_sync_at', 'ASC')
            ->get();
        \Log::info('FOUND ' . $products->count());
        foreach ($products as $product) {
            $result = $this->syncProduct($product);
            if ($result && !$product->is_parent && $product->price && $product->price->to_sync) {
                $this->syncPrice($product->price, true);
            } else if ($result && $product->is_parent) {
                $childProducts = $product->childProducts()->get();
                foreach ($childProducts as $childProduct) {
                   $this->syncPrice($childProduct->price, true);
                }
            }
        }
    }

    /**
     * Forces a complete product sync with shopify
     *
     * @param Product $product
     * @return boolean Success
     */
    public function productSyncForced($product) {
        $result = $this->syncProduct($product);
        if ($result && !$product->is_parent && $product->price) {
            $product->price->sync_data = null;
            $this->syncPrice($product->price, true);
        } else if ($result && $product->is_parent) {
            $childProducts = $product->childProducts()->get();
            foreach ($childProducts as $childProduct) {
                $this->syncPrice($childProduct->price, true);
            }
        }
        return $result;
    }

    /**
     * Updates product price/inventory information in Shopify for the given Price
     * Only updates according to sync_data flags to minimize Shopify API calls
     *
     * @param Price $price
     * @param boolean $noTags - whether not to update product tags
     * @return bool - Success status
     */
    public function syncPrice(Price $price, $noTags = false)
    {
        $product = $price->product;

        \Log::info('Updating product price and inventory: ' . $product->sap_matnr);

        if ($product->parent_id) {
            $variantId = $product->child_variant_id;
            $inventoryItemId = $product->child_inventory_item_id;
            $productId = $product->parent->shopify_id;
        } else {
            $variantId = $product->variant_id;
            $inventoryItemId = $product->inventory_item_id;
            $productId = $product->shopify_id;
        }
        \Log::debug('Variant: ' . $variantId . ' Inventory item: ' . $inventoryItemId);

        if (!$variantId) {
            \Log::warning('Unable to sync product price: product has no shopify info: ' . $product->sap_matnr);
            return false;
        }

        $syncData = isset($price->sync_data) ? $price->sync_data : [];

        if (!isset($syncData['toSync']['price']) || $syncData['toSync']['price'] === true) {
            \Log::info('Updating product price: ' . $product->sap_matnr);
            $variantData = [
                'id' => 'gid://shopify/ProductVariant/' . $variantId,
                'price' => $price->sap_price,
            ];

            $variantData['compareAtPrice'] = $price->sap_price_prev;
            \Log::debug('Sending Price Update: ' . 'gid://shopify/Product/' . $productId  . ' -- ' . json_encode($variantData));
            $query = '
                mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
                    productVariantsBulkUpdate(productId: $productId, variants: $variants) {
                        userErrors {
                            field
                            message
                        }
                    }
                }
            ';
            $response = $this->api->graph($query, [
                'productId' => 'gid://shopify/Product/' . $productId,
                'variants' => [$variantData]
            ]);
            \Log::debug('Sending Price Update RESPONSE: ' . serialize($response['body']));
            if ($response['status'] !== 200) {
                \Log::error('Unable to update shopify variant: ' .  serialize($response['body']));
                return false;
            }
        }

        if (!isset($syncData['toSync']['info']) || $syncData['toSync']['info'] === true) {
            \Log::info('Updating product price info: ' . $product->sap_matnr);
            $priceInfo = [
                'price_type' => $price->sap_price_type,
                'lead_time' => $price->sap_lead_time,
                'disc_start' => $price->sap_disc_start ? $price->sap_disc_start->format('d-m-Y') : null,
                'disc_end' => $price->sap_disc_end ? $price->sap_disc_end->format('d-m-Y') : null,
                'ref' => $product->sap_matnr * 1,
                'avail' => $price->sap_avail,
            ];
            $metafield = $this->upsertObjectMetafield('productVariant', $variantId, 'info', json_encode($priceInfo), 'json_string');

            if (!$metafield) {
                \Log::error('Unable to update shopify price info meta - variant ID: ' .  $variantId);
                return false;
            }
        }

        if (!isset($syncData['toSync']['quant']) || $syncData['toSync']['quant'] === true) {
            \Log::info('Updating product inventory: ' . $product->sap_matnr);

            $newQuant = round($price->sap_quant * 1);
            if ($price->sap_avail === '1' || $price->sap_avail === '3') {
                $newQuant = 0;
            }
            $inventoryData = [
                'inventory_item_id' => $inventoryItemId,
                'location_id' => config('shopify-app.inventory_location_id') * 1,
                'available' => $newQuant
            ];

            $response = $this->api->rest('POST', '/admin/inventory_levels/set.json', [
                'json' => $inventoryData
            ]);

            if ($response['status'] !== 200) {
                \Log::error('Unable to update shopify inventory: ' .  serialize($response['body']));
                return false;
            }
        }

        if (!$noTags && !$product->parent_id && (!isset($syncData['toSync']['avail']) || $syncData['toSync']['avail'] === true)) {
            if (!$this->updateProductTags($product)) {
                return false;
            }
        }

        $syncData['toSync'] = [
            'price' => false,
            'quant' => false,
            'avail' => false,
            'info' => false
        ];
        $price->sync_data = $syncData;
        $price->to_sync = false;
        $price->save();
        return true;
    }

    /**
     * Updates shopify product tags according to current product context (collection, availability, etc)
     *
     * @param Product $product
     * @return bool - status
     */
    protected function updateProductTags(Product $product)
    {
        \Log::info('Updating product tags: ' . $product->sap_matnr);
        $productTags = $this->getProductTags($product);
        $response = $this->api->rest('PUT', '/admin/products/' . $product->shopify_id . '.json', [
            'json' => [
                'product' => [
                    'id' => $product->shopify_id,
                    'tags' => $productTags
                ]
            ]
        ]);
        if ($response['status'] !== 200) {
            \Log::error('Unable to update shopify product tags: ' .  serialize($response['body']));
            return false;
        }
        return true;
    }

    /**
     * Generates tags for a product based on the products current context (collection, availability, etc)
     *
     * @param Product $product
     * @return array - Product tags
     */
    protected function getProductTags(Product $product)
    {
        $tags = [];
        $tags[] = $product->sap_range;
        if ($product->brand && $product->brand->sap_brand != '-') {
            $tags[] = $product->brand->sap_descr;
        }

        if ($product->is_parent) {
            $parentAvailLevel = 1; // 1 - unavailable, 2 - notification, 3 - available, 4 - in-stock
            $sale = false;
            $promotion = false;
            foreach($product->childProducts as $childProduct) {
                if (!$childProduct->price || $childProduct->disabled) {
                    continue;
                }
                if ($childProduct->price->sap_avail === '2' && $childProduct->price->sap_quant > 0 && $childProduct->price->sap_lead_time === 0) {
                    $parentAvailLevel = max($parentAvailLevel, 4);
                }
                if ($childProduct->price->sap_avail === '2' && $childProduct->price->sap_quant > 0) {
                    $parentAvailLevel = max($parentAvailLevel, 3);
                }
                if ($childProduct->price->sap_avail === '3') {
                    $parentAvailLevel = max($parentAvailLevel, 2);
                }
                if ($childProduct->price->sap_price_type === 2) {
                    $promotion = true;
                }
                if ($childProduct->price->sap_price_type === 3) {
                    $sale = true;
                }
                // SAP tags
                if ($childProduct->sap_tags) {
                    $sapTags = explode(',', $childProduct->sap_tags);
                    foreach ($sapTags as $sapTag) {
                        $sapTag = trim($sapTag);
                        if ($sapTag && !in_array($sapTag, $tags)) {
                            $tags[] = $sapTag;
                        }
                    }
                }
            }
            switch ($parentAvailLevel) {
                case 1:
                    $tags[] = 'unavailable-online';
                    break;
                case 2:
                    $tags[] = 'notification';
                    break;
                case 3:
                    $tags[] = 'available-online';
                    break;
                case 4:
                    $tags[] = 'available-online';
                    $tags[] = 'in-stock';
                    break;
            }
            if ($sale || $promotion) {
                $tags[] = 'promotion-or-sale';
            }
            if ($sale) {
                $tags[] = 'sale';
            }
            if ($promotion) {
                $tags[] = 'promotion';
            }
        } else if ($product->price) {
            if ($product->price->sap_avail === '1') {
                $tags[] = 'unavailable-online';
            }
            if ($product->price->sap_avail === '2' && $product->price->sap_quant > 0) {
                $tags[] = 'available-online';
            }
            if ($product->price->sap_avail === '3') {
                $tags[] = 'notification';
            }
            if ($product->price->sap_avail === '2' && $product->price->sap_quant > 0 && $product->price->sap_lead_time === 0) {
                $tags[] = 'in-stock';
            }
            if ($product->price->sap_price_type === 2) {
                $tags[] = 'promotion';
                $tags[] = 'promotion-or-sale';
            }
            if ($product->price->sap_price_type === 3) {
                $tags[] = 'sale';
                $tags[] = 'promotion-or-sale';
            }
        }

        if (!empty(self::BSTAT_TAG_MAP[$product->sap_bstat])) {
            $tags[] = self::BSTAT_TAG_MAP[$product->sap_bstat];
        }

        if (!empty($product->sap_saiso)) {
            $tags[] = 'season-' . strtolower($product->sap_saiso);
        }

        if ($product->is_parent) {
            $tags[] = 'parent-product';
            $tags[] = ($product->sap_matnr * 1) . '';
        }

        $category = $product->category;
        if ($category) {
            $categoryLang = $category->languages->findByLocale($this->defaultLang);
            $tags[] = str_replace(',', '', $categoryLang->sap_descr);

            $parentCat = $category->parent;
            if ($parentCat) {
                $parentCategoryLang = $parentCat->languages->findByLocale($this->defaultLang);
                $tags[] = str_replace(',', '', $parentCategoryLang->sap_descr);
            }
        }

        // SAP tags
        if ($product->sap_tags) {
            $sapTags = explode(',', $product->sap_tags);
            foreach ($sapTags as $sapTag) {
                $sapTag = trim($sapTag);
                if ($sapTag && !in_array($sapTag, $tags)) {
                    $tags[] = $sapTag;
                }
            }
        }

        return $tags;
    }

    /**
     * Gets a Shopify metafield unique id (Gid) based on the owning object's type, id and metafield key and namespace
     *
     * @param string $objectType
     * @param [type] $objectId
     * @param [type] $key
     * @param string $namespace
     * @return string Metafield Gid
     */
    protected function getObjectMetafieldGid($objectType, $objectId, $key, $namespace = 'eae')
    {
        $objectGid = 'gid://shopify/' . ucfirst($objectType) . '/' . $objectId;
        $query = '
            {
                ' . $objectType . '(id: "' . $objectGid . '") {
                    metafield(namespace: "' . $namespace . '", key: "' . $key . '") {
                        id
                    }
                }
            }
        ';
        $result = $this->api->graph($query);

        return !empty($result['body']['data'][$objectType]['metafield'])
            ? $result['body']['data'][$objectType]['metafield']['id']
            : null;
    }

    /**
     * Creates or updates a Shopify metafield for a given shopify object
     *
     * @param [type] $objectType
     * @param [type] $objectId
     * @param [type] $key
     * @param [type] $value
     * @param string $valueType
     * @param string $namespace
     * @return array - Metafield data
     */
    protected function upsertObjectMetafieldOLD($objectType, $objectId, $key, $value, $valueType = 'string', $namespace = 'eae')
    {
        $objectClass = ucfirst($objectType);
        $objectGid = 'gid://shopify/' . $objectClass . '/' . $objectId;
        $metaGid = $this->getObjectMetafieldGid($objectType, $objectId, $key, $namespace);

        $query = '
            mutation ' . $objectType . 'Update($input: ' . $objectClass . 'Input!) {
                ' . $objectType . 'Update(input: $input) {
                    ' . $objectType . ' {
                        metafield(namespace: "' . $namespace . '", key: "' . $key . '") {
                            id
                            namespace
                            key
                            value
                            type
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'input' => [
                'id' => $objectGid,
                'metafields' => [
                    'id' => $metaGid,
                    'namespace' => $namespace,
                    'key' => $key,
                    'value' => $value,
                    'type' => $valueType
                ]
            ]
        ]);
        if ($response['status'] !== 200 && $response['status'] !== 201) {
            return false;
        }

        try {
            $out = $response['body']['data'][$objectType . 'Update'][$objectType]['metafield'];
        } catch (\Throwable $e) {
            \Log::error(json_encode($response));
            \Log::error($e);
            return false;
        }
        return $out;
    }

    /**
     * Creates or updates a Shopify metafield for a given shopify object
     *
     * @param [type] $objectType
     * @param [type] $objectId
     * @param [type] $key
     * @param [type] $value
     * @param string $valueType
     * @param string $namespace
     * @return array - Metafield data
     */
    protected function upsertObjectMetafield($objectType, $objectId, $key, $value, $valueType = 'string', $namespace = 'eae')
    {
        $objectClass = ucfirst($objectType);
        $objectGid = 'gid://shopify/' . $objectClass . '/' . $objectId;
        $metaGid = $this->getObjectMetafieldGid($objectType, $objectId, $key, $namespace);

        $query = '
            mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
                metafieldsSet(metafields: $metafields) {
                    metafields {
                      id
                      key
                      namespace
                      value
                      type
                    }
                    userErrors {
                        field
                        message
                        code
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'metafields' => [
                [
                    'ownerId' => $objectGid,
                    'namespace' => $namespace,
                    'key' => $key,
                    'value' => $value,
                    'type' => $valueType
                ]
            ]
        ]);
        if ($response['status'] !== 200 && $response['status'] !== 201) {
            return false;
        }

        try {
            $out = $response['body']['data']['metafieldsSet']['metafields'][0];
        } catch (\Throwable $e) {
            \Log::error(json_encode($response));
            \Log::error($e);
            return false;
        }
        return $out;
    }

    /**
     * Registers a shopify translation of the specified fields for a given resource Gid
     *
     * @param string $resourceGid
     * @param string $languageCode
     * @param array $fields
     * @return bool - Success
     */
    protected function registerTranslations(string $resourceGid, string $languageCode, array $fields)
    {
        $query = '
            mutation translationsRegister($resourceId: ID!, $translations: [TranslationInput!]!) {
                translationsRegister(resourceId: $resourceId, translations: $translations) {
                    translations {
                        locale
                        key
                        value
                    }
                    userErrors {
                        code
                        field
                        message
                    }
                }
            }
        ';
        $translations = [];
        foreach ($fields as $field) {
            $translations[] = [
                'key' => $field['key'],
                'locale' => $languageCode,
                'value' => $field['value'],
                'translatableContentDigest' => hash('sha256', $field['originalValue'])
            ];
        }
        $result = $this->api->graph($query, [
            'resourceId' => $resourceGid,
            'translations' => $translations
        ]);

        return $result['status'] === 200;
    }

    /**
     * Registers a specific translation for an object
     *
     * @param string $resourceGid
     * @param string $languageCode
     * @param string $key
     * @param string $value
     * @param string $originalValue
     * @return bool - success status
     */
    protected function registerTranslation(string $resourceGid, string $languageCode, string $key, string $value, string $originalValue)
    {
        return $this->registerTranslations($resourceGid, $languageCode, [[
            'key' => $key,
            'value' => $value,
            'originalValue' => $originalValue
        ]]);
    }

    protected function deleteProductVariants(Product $product, array $variantGids)
    {
        $query = '
            mutation productVariantsBulkDelete($productId: ID!, $variantsIds: [ID!]!) {
                productVariantsBulkDelete(productId: $productId, variantsIds: $variantsIds) {
                    product {
                        id
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'productId' => 'gid://shopify/Product/' . $product->shopify_id,
            'variantsIds' => $variantGids
        ]);

        if ($response['status'] !== 200) {
            \Log::error('Unable to delete shopify variants: ' .  serialize($response['body']));
            return false;
        }

        if (count($response['body']['data']['productVariantsBulkDelete']['userErrors'])) {
            \Log::error('Problem deleting shopify variants: ' .  json_encode($response['body']['data']['productVariantsBulkDelete']['userErrors']));
            return false;
        }
        \Log::debug('Product variants deleted: ' .  serialize($response['body']));
        return true;
    }

    protected function updateProductVariants(Product $product, array $variants)
    {
        $query = '
            mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
                productVariantsBulkUpdate(productId: $productId, variants: $variants) {
                    productVariants {
                        id
                        title
                        inventoryItem {
                            id
                        }
                        translations: metafield(namespace: "eae", key: "translations") {
                            id
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'productId' => 'gid://shopify/Product/' . $product->shopify_id,
            'variants' => $variants
        ]);

        if ($response['status'] !== 200) {
            \Log::error('Unable to update shopify variants: ' .  serialize($response['body']));
            return false;
        }

        if (count($response['body']['data']['productVariantsBulkUpdate']['userErrors'])) {
            \Log::error('Problem updating shopify variants: ' .  json_encode($response['body']['data']['productVariantsBulkUpdate']['userErrors']));
            return false;
        }
        \Log::debug('Product variants updated: ' .  serialize($response['body']));
        return $response['body']['data']['productVariantsBulkUpdate']['productVariants'];
    }

    protected function createProductVariants(Product $product, array $variants)
    {
        $query = '
            mutation productVariantsBulkCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
                productVariantsBulkCreate(productId: $productId, variants: $variants) {
                    productVariants {
                        id
                        title
                        inventoryItem {
                            id
                        }
                        translations: metafield(namespace: "eae", key: "translations") {
                            id
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'productId' => 'gid://shopify/Product/' . $product->shopify_id,
            'variants' => $variants
        ]);

        if ($response['status'] !== 200) {
            \Log::error('Unable to create shopify variants: ' .  serialize($response['body']));
            return false;
        }

        if (count($response['body']['data']['productVariantsBulkCreate']['userErrors'])) {
            \Log::error('Problem creating shopify variants: ' .  json_encode($response['body']['data']['productVariantsBulkCreate']['userErrors']));
            return false;
        }
        \Log::debug('Product variants created: ' .  serialize($response['body']));
        return $response['body']['data']['productVariantsBulkCreate']['productVariants'];
    }

    protected function syncProductVariants(Product $product, array $currentVariants)
    {
        $variants = $this->getProductVariants($product);
        if (empty($variants)) {
            return false;
        }

        $toDelete = [];
        $toUpdate = [];
        $toCreate = [];

        $existingVariantIds = array_map(function ($variant) use ($variants, &$toUpdate, &$toDelete) {
            $baseVariants = array_filter($variants, function ($v) use ($variant) {
                return !empty($v['id']) && $v['id'] === $variant['node']['id'];
            });

            if (sizeof($baseVariants) == 0) {
                $toDelete[] = $variant['node']['id'];
            } else {
                $toUpdate[] = array_pop($baseVariants);
            }
            return $variant['node']['id'];
        }, $currentVariants);

        foreach ($variants as $variant) {
            if (empty($variant['id']) || !in_array($variant['id'], $existingVariantIds)) {
                // To create
                $toCreate[] = $variant;
            }
        }

        $outVariants = [];

        if (!empty($toUpdate)) {
            $updatedVariants = $this->updateProductVariants($product, $toUpdate);
            if (!$updatedVariants) {
                return false;
            }
            $outVariants = array_merge($outVariants, $updatedVariants->toArray());
        }

        if (!empty($toCreate)) {
            $createdVariants = $this->createProductVariants($product, $toCreate);
            if (!$createdVariants) {
                return false;
            }
            $outVariants = array_merge($outVariants, $createdVariants->toArray());
        }

        if (!empty($toDelete)) {
            $this->deleteProductVariants($product, $toDelete);
        }

        return $outVariants;
    }

    /**
     * Generate variant data for a given product (not a child product!)
     *
     * @param Product $product
     * @return array - Variant data array
     */
    protected function getProductVariants(Product $product)
    {
        if (!$product->is_parent) {
            $out = [
                'inventoryItem' => [
                    'tracked' => true,
                ]
            ];
            if ($product->variant_id) {
                $out['id'] = 'gid://shopify/ProductVariant/' . $product->variant_id;
            }
            return [$out];
        }
        $childProducts = $product->childProducts()
            ->where('disabled', false)
            ->orderBy('child_order', 'asc')
            ->get();
        if ($childProducts->count() === 0) {
            return [
                [
                    'inventoryItem' => [
                        'tracked' => true,
                    ]
                ]
            ];
        }
        $variants = [];
        foreach ($childProducts as $childProduct) {
            $variantName = $this->getShopifyProductName($childProduct, $this->defaultLang);

            $prodLang = $childProduct->languages->findByLocale($this->defaultLang);
            $translation = [
                'descr' => $prodLang->sap_descr,
                'specs' => $prodLang->sap_specs,
                'dimen' => $prodLang->sap_dimen,
            ];

            $translationsMeta = [
                'key' => 'translations',
                'value' => json_encode($translation),
                'type' => 'json_string',
                'namespace' => 'eae'
            ];

            $variant = [
                'optionValues' => [
                    'optionName' => 'Title',
                    'name' => $variantName
                ],
            ];

            if ($childProduct->child_variant_id) {
                $translationsGid = $this->getObjectMetafieldGid('productVariant', $childProduct->child_variant_id, 'translations');
                if ($translationsGid) {
                    $translationsMeta['id'] = $translationsGid;
                }

                $variant['id'] = 'gid://shopify/ProductVariant/' . $childProduct->child_variant_id;
            }
            $variant['inventoryItem'] = [
                'tracked' => true,
            ];
            $variant['metafields'] = [$translationsMeta];
            $variants[] = $variant;
        }

        return $variants;
    }

    public function publishProduct(Product $product)
    {
        $publicationId =  config('shopify-app.online_store_publication_gid');
        if (!$publicationId) {
            \Log::error('Unable to publish shopify product: publication id not found');
            throw new \Exception('Unable to publish shopify product: publication id not found');
        }

        $query = '
            mutation productPublish($input: ProductPublishInput!) {
                productPublish(input: $input) {
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

        $response = $this->api->graph($query, [
            'input' => [
                'id' => 'gid://shopify/Product/' . $product->shopify_id,
                'productPublications' => [
                    'publicationId' => $publicationId
                ]
            ]
        ]);

        if ($response['status'] !== 200) {
            \Log::error('Unable to publish shopify product: ' .  serialize($response['body']));
            return false;
        }

        if (count($response['body']['data']['productPublish']['userErrors'])) {
            \Log::error('Problem publishing shopify product: ' .  json_encode($response['body']['data']['productPublish']['userErrors']));
            return false;
        }
        \Log::debug('Product published: ' .  serialize($response['body']));
        return true;
    }

    /**
     * Syncs a product main data (title, description, handle, etc) with shopify (to shopify)
     *
     * @param Product $product
     * @return bool - Success
     */
    public function syncProduct(Product $product)
    {
        \Log::info('Syncing Product ' . $product->sap_matnr . ' to Shopify...');
        $productName = $this->getShopifyProductName($product, $this->defaultLang);
        if ($product->is_parent) {
            $handle = StringHelper::urlize(ltrim($product->sap_matnr, '0') . ' ' . $productName);
        } else {
            $handle = StringHelper::urlize($productName);
        }

        $shopifyProduct = [
            'title' => $productName,
            'handle' => $handle,
            'vendor' => 'Area Store',
            'tags' => $this->getProductTags($product),
            //'variants' => $this->getProductVariants($product),
            //'published' => true,
        ];

        $translations = [];
        foreach ($this->langs as $lang) {
            $prodLang = $product->languages->findByLocale($lang);
            if ($this->defaultLang === $lang) {
                $shopifyProduct['descriptionHtml'] = $prodLang->sap_specs;
            }
            $translations[$lang] = [
                'descr' => $prodLang->sap_descr,
                'specs' => $prodLang->sap_specs,
                'dimen' => $prodLang->sap_dimen,
            ];
        }

        $info = [
            'bstat' => $product->sap_bstat,
            'saiso' => $product->sap_saiso,
            'ref' => $product->sap_matnr * 1,
            'range' => $product->sap_range,
            'store' => $product->sap_store
        ];

        if ($product->brand && $product->brand->sap_brand != '-') {
            $info['brand_code'] = $product->brand->sap_brand;
            $info['brand_descr'] = $product->brand->sap_descr;
        }

        $meta = [
            'info' => [
                'key' => 'info',
                'value' => json_encode($info),
                'type' => 'json_string',
                'namespace' => 'eae'
            ],
            'translations' => [
                'key' => 'translations',
                'value' => json_encode($translations[$this->defaultLang]),
                'type' => 'json_string',
                'namespace' => 'eae'
            ]
        ];

        $mutation = 'productCreate';
        $inputType = 'ProductCreateInput';
        if ($product->shopify_id) {
            // Tries to get product metafields gids to allow metafield update
            $translationsGid = $this->getObjectMetafieldGid('product', $product->shopify_id, 'translations');
            $infoGid = $this->getObjectMetafieldGid('product', $product->shopify_id, 'info');
            $mutation = 'productUpdate';
            $inputType = 'ProductUpdateInput';

            $shopifyProduct['id'] = 'gid://shopify/Product/' . $product->shopify_id;
            $meta['translations']['id'] = $translationsGid;
            $meta['info']['id'] = $infoGid;

            if ($product->to_enable) {
                $shopifyProduct['status'] = 'DRAFT';
                $product->disabled = false;
                $product->to_enable = false;
            }

            if ($product->is_parent && $product->childProducts()->where('disabled', false)->count() === 0) {
                $shopifyProduct['status'] = 'DRAFT';
            }
        } else {
            $shopifyProduct['status'] = 'DRAFT';
            $product->disabled = false;
            $product->to_enable = false;
        }
        $shopifyProduct['metafields'] = array_values($meta);
        // dd($shopifyProduct);
        $query = '
            mutation ' . $mutation . '($input: ' . $inputType . '!) {
                ' . $mutation . '(product: $input) {
                    product {
                        id
                        info: metafield(namespace: "eae", key: "info") {
                            id
                        }
                        translations: metafield(namespace: "eae", key: "translations") {
                            id
                        }
                        options {
                            id
                            name
                        }
                        variants(first:30) {
                            edges {
                                node {
                                    id
                                    title
                                    inventoryItem {
                                        id
                                    }
                                    translations: metafield(namespace: "eae", key: "translations") {
                                        id
                                    }
                                }
                            }
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'input' => $shopifyProduct
        ]);

        \Log::debug('Set to Shopify: ' . json_encode($shopifyProduct));

        if (($response['status'] !== 200 && $response['status'] !== 201) || !empty($response['errors'])) {
            \Log::error('Unable to create / update shopify product: ' .  serialize($response['body']));
            return false;
        }

        if (count($response['body']['data'][$mutation]['userErrors'])) {
            \Log::error('Problem creating / updating shopify product: ' .  json_encode($response['body']['data'][$mutation]['userErrors']));
        }

        \Log::info('Query cost: ' . json_encode($response['body']['extensions']['cost']));

        $responseProduct = $response['body']['data'][$mutation]['product'];

        \Log::debug('Got from Shopify: ' . json_encode($response));

        if (!$responseProduct) {
            \Log::error('Unable to create / update shopify product: ' .  serialize($response['body']));
            return false;
        }

        if ($mutation === 'productCreate') {
            if ($product->is_parent) {
                $firstChild = $product->childProducts()
                    ->where('disabled', false)
                    ->orderBy('child_order', 'asc')
                    ->first();
                if ($firstChild) {
                    $firstChild->child_variant_id = StringHelper::getIdFromGid($responseProduct['variants']['edges'][0]['node']['id']);
                    $firstChild->child_inventory_item_id = StringHelper::getIdFromGid($responseProduct['variants']['edges'][0]['node']['inventoryItem']['id']);
                    $firstChild->save();
                }
            } else {
                $product->variant_id = StringHelper::getIdFromGid($responseProduct['variants']['edges'][0]['node']['id']);
                $product->inventory_item_id = StringHelper::getIdFromGid($responseProduct['variants']['edges'][0]['node']['inventoryItem']['id']);
            }
            $product->shopify_id = StringHelper::getIdFromGid($responseProduct['id']);
            $product->save();

            $this->publishProduct($product);
        }

        $syncedVariants = $this->syncProductVariants($product, $responseProduct['variants']['edges']->toArray());
        if (!$syncedVariants) {
            return false;
        }

        if ($product->is_parent) {
            $childs = $product->childProducts()
                ->where('disabled', false)
                ->orderBy('child_order', 'asc')
                ->get();
            foreach ($childs as $i => $childProduct) {
                $varName = $this->getShopifyProductName($childProduct, $this->defaultLang);

                $found = array_filter($syncedVariants, function ($v) use ($varName) {
                    return !empty($v['title']) && $v['title'] === $varName;
                });

                if (empty($found)) {
                    \Log::error('Unable to find variant ' . $varName . ' in shopify response');
                    continue;
                }
                $childVariant = array_pop($found);

                if ($childProduct->shopify_id) {
                    // Was a product itself before, so shopify product needs to be archived
                    $this->archiveProduct($childProduct);
                }

                $childProduct->child_variant_id = StringHelper::getIdFromGid($childVariant['id']);
                $childProduct->child_inventory_item_id = StringHelper::getIdFromGid($childVariant['inventoryItem']['id']);

                if ($childProduct->price) {
                    \Log::debug('SET ' . $childProduct->sap_matnr . ' to sync price');
                    $childProduct->price->to_sync = true;
                    $childProduct->price->sync_data = [];
                    $childProduct->price->save();
                }
                $childProduct->to_enable = false;
                $childProduct->to_sync = false;
                $childProduct->save();
            }
            $product->variant_id = null;
            $product->inventory_item_id = null;
        } else {
            $responseVariant = $responseProduct['variants']['edges'][0]['node'];
            $product->variant_id = StringHelper::getIdFromGid($responseVariant['id']);
            $product->inventory_item_id = StringHelper::getIdFromGid($responseVariant['inventoryItem']['id']);
        }

        $product->to_sync = false;
        $product->save();
        \Log::info('Created / Updated shopify product ' . $product->sap_matnr . ': ' . $product->shopify_id . ' variantId: ' . $product->variant_id);

        // Set translations
        foreach ($this->langs as $lang) {
            if ($lang === $this->defaultLang) {
                // no transaltion needed for default
                continue;
            }
            $fields = [
                [
                    'key' => 'title',
                    'originalValue' => $shopifyProduct['title'],
                    'value' => $this->getShopifyProductName($product, $lang)
                ]
            ];
            if (!empty($translations[$lang]['specs'])) {
                $fields[] = [
                    'key' => 'body_html',
                    'originalValue' => $shopifyProduct['descriptionHtml'],
                    'value' => $translations[$lang]['specs']
                ];
            }
            $this->registerTranslations('gid://shopify/Product/' . $product->shopify_id, $lang, $fields);

            // Meta translation
            $value = json_encode($translations[$lang]);
            $originalValue = json_encode($translations[$this->defaultLang]);

            $this->registerTranslation($responseProduct['translations']['id'], $lang, 'value', $value, $originalValue);

            // Child products translations
            $childs = $product->childProducts()
                ->where('disabled', false)
                ->orderBy('child_order', 'asc')
                ->get();
            foreach ($childs as $i => $childProduct) {
                $varName = $this->getShopifyProductName($childProduct, $this->defaultLang);
                $found = array_filter($syncedVariants, function ($v) use ($varName) {
                    return !empty($v['title']) && $v['title'] === $varName;
                });

                if (empty($found)) {
                    \Log::error('Unable to find variant ' . $varName . ' in shopify response');
                    continue;
                }
                $childVariant = array_pop($found);

                $prodLang = $childProduct->languages->findByLocale($lang);
                $prodDefaultLang = $childProduct->languages->findByLocale($this->defaultLang);
                $translation = [
                    'descr' => $prodLang->sap_descr,
                    'specs' => $prodLang->sap_specs,
                    'dimen' => $prodLang->sap_dimen,
                ];
                $value = json_encode($translation);
                $originalValue = json_encode([
                    'descr' => $prodDefaultLang->sap_descr,
                    'specs' => $prodDefaultLang->sap_specs,
                    'dimen' => $prodDefaultLang->sap_dimen,
                ]);

                $baseMetaId = $childVariant['translations']['id'];
                $this->registerTranslation($baseMetaId, $lang, 'value', $value, $originalValue);
            }
        }
        return true;
    }

    /**
     * Gets collects (collection products) for a given collection id from Shopify
     *
     * @param [type] $collectionId
     * @return array
     */
    public function getCollectionCollects($collectionId)
    {
        $collects = [];
        $link = null;
        $maxPages = 10;
        $i = 0;
        do {
            $i++;
            $query = [
                'limit' => 250
            ];
            if ($link) {
                $query['page_info'] = $link;
            } else {
                $query['collection_id'] = $collectionId;
            }
            $response = $this->api->rest('GET', '/admin/collects.json', [
                'query' => $query
            ]);

            if ($response['status'] !== 200) {
                \Log::error('Unable to get collection collects - collection: ' . $collectionId);
                return false;
            }

            $link = !empty($response['link']['next']) ? $response['link']['next'] : null;
            foreach ($response['body']['collects'] as $collect) {
                array_push($collects, $collect);
            }
        } while ($link !== null || $i > $maxPages);
        return $collects;
    }

    /**
     * Syncs collections to Shopify
     *
     * @param boolean $force - Forces sync even if collection not marked for sync
     * @return void
     */
    public function syncCollections(bool $force = false)
    {
        \Log::info('Syncing collections with Shopify...');
        $query = Category::where('type', Category::CATEGORY_TYPE_SUBCAT)
            ->orderBy('last_sync_at', 'ASC');

        if (!$force) {
            $query->where('to_sync', true);
        }

        $cats = $query->get();
        \Log::info('Found ' . $cats->count()  . ' collection(s) to sync.');
        foreach ($cats as $cat) {
            if (!$this->syncCollection($cat)) {
                continue;
            }
            if (!$this->syncCollectionItems($cat)) {
                continue;
            }
            $cat->to_sync = false;
            $cat->save();
        }
    }

    /**
     * Syncs a given collection items to Shopify
     *
     * @param Category $category
     * @return bool - Success
     */
    public function syncCollectionItems(Category $category)
    {
        \Log::info('Syncing collection items...');
        $collects = $this->getCollectionCollects($category->shopify_id);
        if ($collects === false) {
            return false;
        }
        $toKeep = [];
        $toRemove = [];
        $toAdd = [];
        $catProducts = $category->products()->where('disabled', false)->get()->keyBy('shopify_id');
        foreach ($collects as $collect) {
            if (empty($catProducts[$collect['product_id']])) {
                $toRemove[] = $collect;
            } else {
                $toKeep[$collect['product_id']] = $collect;
            }
        }
        foreach ($catProducts as $product) {
            if ($product->shopify_id && empty($toKeep[$product->shopify_id])) {
                $toAdd[] = [
                    'product_id' => $product->shopify_id
                ];
            }
        }

        \Log::info('Will remove: ' . sizeof($toRemove) . ' Will add: ' . sizeof($toAdd) . ' Will keep: ' . sizeof($toKeep));

        foreach ($toRemove as $collect) {
            $response = $this->api->rest('DELETE', '/admin/collects/' . $collect['id'] . '.json');
            if ($response['status'] === 200) {
                \Log::info('Removed product ' . $collect['product_id'] . ' from collection ' . $category->shopify_id);
            } else {
                \Log::error('Error removing product ' . $collect['product_id'] . ' from collection ' . $category->shopify_id . ': ' . serialize($response['body']));
            }
        }
        if (!empty($toAdd)) {
            $response = $this->api->rest('PUT', '/admin/custom_collections/' . $category->shopify_id . '.json',[
                'json' => [
                    'custom_collection' => [
                        'id' => $category->shopify_id,
                        'collects' => $toAdd
                    ]
                ]
            ]);
            if ($response['status'] === 200) {
                \Log::info('Added ' . sizeof($toAdd) . ' products to collection ' . $category->shopify_id);
            } else {
                \Log::error('Error adding items to colllection ' . $category->shopify_id . ': ' . serialize($response['body']));
            }
        }
        return true;
    }

    /**
     * Syncs collection info to shopify
     *
     * @param Category $category
     * @return bool - Status
     */
    public function syncCollection(Category $category)
    {
        if ($category->type !== Category::CATEGORY_TYPE_SUBCAT) {
            return false;
        }
        $categoryLang = $category->languages->findByLocale($this->defaultLang);
        $collectionData = [
            'title' => $categoryLang->sap_descr,
            'handle' => StringHelper::urlize($categoryLang->sap_descr)
        ];
        $translations = [];
        foreach ($this->langs as $lang) {
            $prodLang = $category->languages->findByLocale($lang);
            $translations[$lang] = [
                'descr' => $prodLang->sap_descr,
                'published' => true
            ];
        }

        $mutation = 'collectionCreate';
        if ($category->shopify_id) {
            // Tries to get collection metafields and, at the same time, verifies that collection exists in shopify
            $collectionData['id'] = 'gid://shopify/Collection/' . $category->shopify_id;
            $mutation = 'collectionUpdate';
        }

        $query = '
            mutation ' . $mutation . '($input: CollectionInput!) {
                ' . $mutation . '(input: $input) {
                    collection {
                        id
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';
        $response = $this->api->graph($query, [
            'input' => $collectionData
        ]);

        if ($response['status'] !== 200 && $response['status'] !== 201 || sizeof($response['body']['data'][$mutation]['userErrors']) > 0) {
            \Log::error('Unable to create / update shopify collection: ' .  serialize($response['body']));
            return false;
        }

        $category->shopify_id = StringHelper::getIdFromGid($response['body']['data'][$mutation]['collection']['id']);
        $category->to_sync = false;
        $category->save();
        \Log::info('Created / Updated shopify collection ' . $category->sap_id . ': ' . $category->shopify_id);

        // Translations
        foreach ($this->langs as $lang) {
            if ($lang === $this->defaultLang) {
                // no transaltion needed for default
                continue;
            }
            $fields = [
                [
                    'key' => 'title',
                    'originalValue' => $collectionData['title'],
                    'value' => $translations[$lang]['descr']
                ]
            ];
            $this->registerTranslations('gid://shopify/Collection/' . $category->shopify_id, $lang, $fields);
        }

        /* Not working still? Shopify mess
        if ($mutation === 'collectionCreate') {
            // Publishes collection
            $this->publishResource('gid://shopify/Collection/' . $category->shopify_id);
        }
        */
        return true;
    }

    /**
     * Published a given resource in shopify
     *
     * @param string $shopifyId - GID
     * @return bool - Success
     */
    protected function publishResource($shopifyId)
    {
        $query = '
            mutation publishablePublish($id: ID!, $input: [PublicationInput!]!) {
                publishablePublish(id: $id, input: $input) {
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

        $response = $this->api->graph($query, [
            'id' => $shopifyId,
            'input' => [
            ]
        ]);

        if ($response['status'] !== 200 && $response['status'] !== 201) {
            \Log::error('Unable to publish resource id (' . $shopifyId . '): ' .  serialize($response['body']));
            return false;
        }
        return true;
    }

    /**
     * Generates Shopify product name based on established rules
     *
     * @param Product $product
     * @param string $languageId
     * @return string
     */
    public function getShopifyProductName(Product $product, string $languageId): string
    {
        $productLang = $product->languages->findByLocale($languageId);
        $name = '';

        if (!$product->is_parent) {
            $name = ltrim($product->sap_matnr, '0');
        }

        if (!$product->parent_id && $product->brand && $product->brand->sap_brand !== '-') {
            $name .= ($name ? ' ' : '') . $product->brand->sap_descr;
        }

        if (!$product->parent_id && $product->sap_range) {
            // Since sap_descr is currently comming with range, we skip this
            $name .= ($name ? ' ' : '') . $product->sap_range;
        }

        if ($productLang->sap_descr) {
            $name .= ' ' . $productLang->sap_descr;
        }

        return mb_strtoupper($name);
    }

    /**
     * Creates a Customer (integration model) from a Shopify customer data
     *
     * @param stdClass $customerData
     * @return Customer
     */
    public function customerCreate(stdClass $customerData): Customer
    {
        DB::beginTransaction();

        try {
            $customer = Customer::where('shopify_id', $customerData->id)
                ->first();
            if (!$customer) {
                $customer = new Customer();
                $customer->shopify_id = $customerData->id;
            }
            $customer->email = $customerData->email;
            $customer->first_name = $customerData->first_name;
            $customer->last_name = $customerData->last_name;
            $customer->phone = $customerData->phone;
            $customer->accepts_marketing = !empty($customerData->email_marketing_consent) && $customerData->email_marketing_consent->state == 'subscribed';
            $customer->note = $customerData->note;
            $customer->to_sync = true;
            $customer->last_sync_at = Carbon::now();
            $customer->save();

            if (!empty($customerData->default_address)) {
                $this->addressCreate($customer, $customerData->default_address);
            }

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            \Log::error($e);
            throw($e);
        }

        return $customer;
    }

    /**
     * Creates an Address entity based on shopify address data
     *
     * @param Customer $customer
     * @param stdClass $addressData
     * @return Address
     */
    public function addressCreate(Customer $customer, stdClass $addressData)
    {
        DB::beginTransaction();

        try {
            $address = Address::where('shopify_id', $addressData->id)
                ->first();

            if (!$address) {
                $address = new Address();
                $address->customer_id = $customer->id;
                $address->shopify_id = $addressData->id;
            }

            $address->first_name = $addressData->first_name ?? '';
            $address->last_name = $addressData->last_name ?? '';
            $address->company = $addressData->company;
            $address->address1 = $addressData->address1 ?? '';
            $address->address2 = $addressData->address2;
            $address->city = $addressData->city;
            $address->province = $addressData->province;
            $address->country = $addressData->country;
            $address->country_code = $addressData->country_code;
            $address->zip = $addressData->zip;
            $address->phone = $addressData->phone;
            $address->is_default = $addressData->default;
            $address->to_sync = true;
            $address->last_sync_at = Carbon::now();
            $address->save();

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            \Log::error($e);
            throw($e);
        }
        return $address;
    }

    /**
     * Gets an order from Shopify and creates/updates it on the integration system
     *
     * @param integer $orderId - Shopify id
     * @return Order
     */
    public function syncShopifyOrder(int $orderId)
    {
        $response = $this->api->rest('GET', '/admin/orders/' . $orderId . '.json');
        if ($response['errors'] || empty($response['body']['order'])) {
            throw new RuntimeException('Error retrieving Shopify order: ' . $response['body']);
        }

        // json trick to convert associative response to objects
        $orderData = json_decode(json_encode($response['body']['order']));
        $order = $this->orderCreate($orderData);
        return $order;
    }

    /**
     * Creates or updates an Order based on a Shopify order data
     *
     * @param stdClass $orderData
     * @return Order
     */
    public function orderCreate(stdClass $orderData)
    {
        DB::beginTransaction();

        try {
            $order = Order::where('shopify_id', $orderData->id)
                ->first();

            if (empty($orderData->customer)) {
                throw new RuntimeException('Shopify Order ' . $orderData->name . ' from shopify has no customer!');
            }

            $customer = Customer::where('shopify_id', $orderData->customer->id)
                ->first();

            if (!$customer) {
                $customer = $this->customerCreate($orderData->customer);
            }

            if (!$order) {
                $order = new Order();
                $order->customer_id = $customer->id;
                $order->shopify_id = $orderData->id;
                $order->order_number = $orderData->name;
            }

            $order->order_created_at = (new Carbon($orderData->created_at))->setTimezone('UTC');
            $order->order_updated_at = $orderData->updated_at ? (new Carbon($orderData->updated_at))->setTimezone('UTC') : null;
            $order->email = $orderData->email;
            $order->financial_status = $orderData->financial_status;

            $order->shipping_name = StringHelper::cleanWhiteSpaces(mb_substr($orderData->shipping_address->name, 0, 254, 'utf-8'));
            $order->shipping_first_name = StringHelper::cleanWhiteSpaces($orderData->shipping_address->first_name);
            $order->shipping_last_name = StringHelper::cleanWhiteSpaces($orderData->shipping_address->last_name);
            $order->shipping_company = $orderData->shipping_address->company;
            $order->shipping_address1 = $orderData->shipping_address->address1;
            $order->shipping_address2 = $orderData->shipping_address->address2;
            $order->shipping_city = $orderData->shipping_address->city;
            $order->shipping_province = $orderData->shipping_address->province;
            $order->shipping_country = $orderData->shipping_address->country;
            $order->shipping_country_code = $orderData->shipping_address->country_code;
            $order->shipping_zip = $orderData->shipping_address->zip;
            $order->shipping_phone = $orderData->shipping_address->phone;

            $order->billing_name = StringHelper::cleanWhiteSpaces(mb_substr($orderData->billing_address->name, 0, 254, 'utf-8'));
            $order->billing_first_name = StringHelper::cleanWhiteSpaces($orderData->billing_address->first_name);
            $order->billing_last_name = StringHelper::cleanWhiteSpaces($orderData->billing_address->last_name);
            $order->billing_company = $orderData->billing_address->company;
            $order->billing_address1 = $orderData->billing_address->address1;
            $order->billing_address2 = $orderData->billing_address->address2;
            $order->billing_city = $orderData->billing_address->city;
            $order->billing_province = $orderData->billing_address->province;
            $order->billing_country = $orderData->billing_address->country;
            $order->billing_country_code = $orderData->billing_address->country_code;
            $order->billing_zip = $orderData->billing_address->zip;
            $order->billing_phone = $orderData->billing_address->phone;

            $order->same_address = empty(array_diff_assoc((array) $orderData->billing_address, (array) $orderData->shipping_address));

            $order->accepts_marketing = $orderData->buyer_accepts_marketing;
            $order->note = $orderData->note;
            $order->note_attributes = $orderData->note_attributes;
            $order->total = $orderData->total_price;

            $totalShipping = 0;
            $totalShippingDiscounts = 0;
            $shippingType = null;
            foreach ($orderData->shipping_lines as $shippingLine) {
                $totalShipping += $shippingLine->price;
                if (!$shippingType) {
                    $shippingType = $shippingLine->title;
                }
                foreach ($shippingLine->discount_allocations as $shippingDiscount) {
                    $totalShippingDiscounts += $shippingDiscount->amount;
                }
            }
            $order->shipping_type = $shippingType;
            $order->shipping_total = $totalShipping;
            $order->shipping_discount = $totalShippingDiscounts;
            $order->tax_id = $this->getNoteAttribute('vat', $orderData);
            $order->customer_type = $this->getNoteAttribute('customer_type', $orderData);
            $order->to_sync = 1;

            $order->save();

            foreach ($orderData->line_items as $i => $lineItemData) {
                $orderItem = OrderItem::where('shopify_id', $lineItemData->id)
                    ->first();

                $product = Product::where('variant_id', $lineItemData->variant_id)
                    ->orWhere('child_variant_id', $lineItemData->variant_id)
                    ->first();

                if (!$product) {
                    throw new RuntimeException('Shopify product ' . $lineItemData->product_id . ' does not exist!');
                }

                if (!$orderItem) {
                    $orderItem = new OrderItem();
                    $orderItem->shopify_id = $lineItemData->id;
                    $orderItem->order_id = $order->id;

                    $orderItem->product_id = $product->id;
                    $orderItem->sap_matnr = $product->sap_matnr;
                }

                $orderItem->pos = ($i +  1);
                $orderItem->quantity = $lineItemData->quantity;
                $orderItem->total = $lineItemData->price * $lineItemData->quantity;

                $totalDiscount = 0;
                foreach ($lineItemData->discount_allocations as $discountAllocation) {
                    $totalDiscount += $discountAllocation->amount;
                }
                $orderItem->discount = $totalDiscount;

                $expectedDate = $order->order_created_at->copy()->addDay($product->price->sap_lead_time);
                $orderItem->expected_date = $expectedDate;
                $orderItem->save();
            }

            DB::commit();

            // Marks order as integrated in shopify
            $this->addOrderTag($order, 'int');

            return $order;
        } catch (\Throwable $e) {
            DB::rollBack();
            \Log::error($e);
            throw($e);
        }
    }

    /**
     * Updates an order's Shopify state (fulfillment and refunds)
     *
     * @param Order $order
     * @return void
     */
    public function orderStatusUpdate(Order $order)
    {
        $itemsToFulfill = [];
        $itemsToRefund = [];
        foreach ($order->orderItems as $item) {
            if ($item->quantity_to_fulfill > 0) {
                $itemsToFulfill[] = $item;
            }

            if ($item->quantity_to_refund > 0) {
                $itemsToRefund[] = $item;
            }
        }

        $success = true;
        if (sizeof($itemsToFulfill) > 0) {
            $success = $this->fulfillOrder($order, $itemsToFulfill);
        }

        if (sizeof($itemsToRefund) > 0 || $order->shipping_to_refund > 0) {
            $success = $this->refundOrder($order, $itemsToRefund) && $success;
        }

        if ($success) {
            $order->to_sync_status = false;
            $order->save();
        }

        return $success;
    }

    /**
     * Refunds an order (items and/or shipping) in Shopify
     *
     * @param Order $order
     * @param Array $itemsToRefund
     * @return bool - Success
     */
    protected function refundOrder(Order $order, Array $itemsToRefund)
    {
        \Log::info('Refunding order Shopify ID nr ' . $order->shopify_id) . '...';
        DB::beginTransaction();
        try {
            $query = '
                mutation refundCreate($input: RefundInput!) {
                    refundCreate(input: $input) {
                        order {
                            id
                        }
                        refund {
                            id
                        }
                        userErrors {
                            field
                            message
                        }
                    }
            }
            ';

            $refundLines = [];
            $refundCalcLines = [];
            foreach ($itemsToRefund as $itemToRefund) {
                $refundLines[] = [
                    'lineItemId' => 'gid://shopify/LineItem/' . $itemToRefund->shopify_id,
                    'quantity' => intval($itemToRefund->quantity_to_refund)
                ];
                if (!$itemToRefund->no_transaction) {
                    $refundCalcLines[] = [
                        'line_item_id' => $itemToRefund->shopify_id,
                        'quantity' => intval($itemToRefund->quantity_to_refund),
                        'restock_type' => 'no_restock'
                    ];
                }
                $itemToRefund->refunded_quantity += $itemToRefund->quantity_to_refund;
                $itemToRefund->quantity_to_refund = 0;
                $itemToRefund->save();
            }

            $calculatedRefund = null;
            if (!empty($refundCalcLines) || ($order->shipping_to_refund > 0 && !$order->no_transaction)) {
                $calculatedRefund = $this->calcRefund($order, $refundCalcLines);
            }

            $refund = [
                'notify' => true,
                'orderId' => 'gid://shopify/Order/' . $order->shopify_id,
                'refundLineItems' => $refundLines,
                'transactions' => []
            ];

            if ($calculatedRefund !== null) {
                foreach ($calculatedRefund['transactions'] as $transaction) {
                    $refund['transactions'][] = [
                        'amount' => $transaction['amount'],
                        'gateway' => $transaction['gateway'],
                        'kind' => 'REFUND',
                        'orderId' => 'gid://shopify/Order/' . $order->shopify_id,
                        'parentId' => 'gid://shopify/OrderTransaction/' . $transaction['parent_id']
                    ];
                }
            }

            if ($order->shipping_to_refund > 0) {
                $refund['shipping'] = [
                    'amount' => $order->shipping_to_refund
                ];

                $order->refunded_shipping += $order->shipping_to_refund;
                $order->shipping_to_refund = 0;
                $order->save();
            }
            \Log::info('Sending to Shopify - refund data: ' . json_encode($refund));
            $response = $this->api->graph($query, [
                'input' => $refund
            ]);
            \Log::info(json_encode($response));
            if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
                $msg = "Unable to refund order in Shopify: " . json_encode($response);
                throw new \RuntimeException($msg);
            }
            if (sizeof($response['body']['data']['refundCreate']['userErrors']) > 0) {
                if (empty($response['body']['data']['refundCreate']['refund'])) {
                    $msg = "Unable to refund order in Shopify: " . json_encode($response);
                    throw new \RuntimeException($msg);
                }

                \Log::error("There was a problem refunding a order in Shopify, but refund was created: " . $response['body']['data']['refundCreate']['refund']['id']);
            }
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            \Log::error($e);
            return false;
        }
        \Log::info('Refunded Shopify ID nr ' . $order->shopify_id);
        return true;
    }

    /**
     * Calculates refund data (in Shopify) needed to refund a given order
     *
     * @param Order $order
     * @param Array $refundItems
     * @return array - Refund calculated data
     */
    protected function calcRefund(Order $order, Array $refundItems)
    {
        $refund = [
            'shipping' => [
                'amount' => $order->shipping_to_refund
            ],
            'refund_line_items' => $refundItems
        ];

        $response = $this->api->rest('POST', '/admin/orders/' . $order->shopify_id . '/refunds/calculate.json', [
            'json' => ['refund' => $refund]
        ]);

        \Log::info('Calculated Refund: ' . json_encode($response));

        if ($response['status'] !== 200) {
            throw new \RuntimeException('Unable to calculate refund for order: ' . $order->shopify_id);
        }

        return $response['body']['refund'];
    }

    /**
     * Fulfills line items for a given order in Shopify
     *
     * @param Order $order
     * @param Array $itemsToFulfill
     * @return bool - Success
     */
    protected function fulfillOrder(Order $order, Array $itemsToFulfill)
    {
        \Log::info('Fulfilling order Shopify ID nr ' . $order->shopify_id) . '...';
        DB::beginTransaction();
        try {
            $fulfillmentOrder = $this->getFulfillmentOrder($order);
            // \Log::info(json_encode($fulfillmentOrder)); die;
            $query = '
                mutation fulfillmentCreateV2($fulfillment: FulfillmentV2Input!) {
                    fulfillmentCreateV2(fulfillment: $fulfillment) {
                        fulfillment {
                            id
                        }
                        userErrors {
                            field
                            message
                        }
                    }
                }
            ';

            $fulfillmentItems = [];
            foreach ($itemsToFulfill as $itemToFulfill) {
                foreach ($fulfillmentOrder['lineItems']['edges'] as $lineItem) {
                    if ($lineItem['node']['lineItem']['id'] === ('gid://shopify/LineItem/' . $itemToFulfill->shopify_id)) {
                        if ($lineItem['node']['remainingQuantity'] < $itemToFulfill->quantity_to_fulfill) {
                            $msg = 'Unable to fulfill line item ' . $itemToFulfill->shopify_id . ': Not enough remaining quantity';
                            throw new \RuntimeException($msg);
                        }
                        $fulfillmentItems[] = [
                            'id' => $lineItem['node']['id'],
                            'quantity' => intval($itemToFulfill->quantity_to_fulfill)
                        ];
                        $itemToFulfill->fulfilled_quantity += $itemToFulfill->quantity_to_fulfill;
                        $itemToFulfill->quantity_to_fulfill = 0;

                        $itemToFulfill->save();
                    }
                }
            }

            $fulfillment = [
                'lineItemsByFulfillmentOrder' => [
                    [
                        'fulfillmentOrderId' => $fulfillmentOrder['id'],
                        'fulfillmentOrderLineItems' => $fulfillmentItems
                    ]
                ],
                'notifyCustomer' => true
            ];

            $response = $this->api->graph($query, [
                'fulfillment' => $fulfillment
            ]);

            if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
                $msg = "Unable to fulfill order in Shopify: " . json_encode($response);
                throw new \RuntimeException($msg);
            }
            if (sizeof($response['body']['data']['fulfillmentCreateV2']['userErrors']) > 0) {
                $msg = "Unable to fulfill order in Shopify: " . json_encode($response);
                throw new \RuntimeException($msg);
            }
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            \Log::error($e);
            return false;
        }
        \Log::info('Order fulfilled - Shopify ID nr ' . $order->shopify_id);
        return true;
    }

    /**
     * Gets Fulfillment Order data needed to create Shopify fulfillments for an order
     *
     * @param Order $order
     * @return array - fulfillment order data
     */
    public function getFulfillmentOrder(Order $order)
    {
        $openFulfillmentId = $this->getOpenFulfillmentOrderId($order);

        $query = '
          query getFulfillmentOrderByID($id: ID!) {
            fulfillmentOrder(id: $id) {
              id
              status
              requestStatus
              supportedActions {
                action
              }
              lineItems(first: 200) {
                edges {
                  node {
                    id
                    totalQuantity
                    remainingQuantity
                    lineItem {
                      id
                    }
                  }
                }
              }
            }
          }
        ';

        $response = $this->api->graph($query, [
            'id' => $openFulfillmentId
        ]);

        if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
            $msg = "Unable to get fulfillment order from shopify: " . json_encode($response['errors']);
            \Log::error($msg);
            throw new RuntimeException($msg);
        }
        return $response['body']['data']['fulfillmentOrder'];
    }

    /**
     * Gets Fulfillment Orders
     *
     * @param Order $order
     * @return array - fulfillment order data
     */
    public function getFulfillmentOrders(Order $order)
    {
        $query = '
          query getOrderByID($id: ID!) {
            order(id: $id) {
              id
              name
              fulfillmentOrders(first: 3) {
                edges {
                  node {
                    id
                    status
                    requestStatus
                    supportedActions {
                      action
                    }
                    lineItems(first: 100) {
                      edges {
                        node {
                          id
                          totalQuantity
                          remainingQuantity
                          lineItem {
                            id
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        ';

        $response = $this->api->graph($query, [
            'id' => 'gid://shopify/Order/' . $order->shopify_id
        ]);

        if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
            $msg = "Unable to get fulfillment order from shopify: " . json_encode($response['errors']);
            \Log::error($msg);
            throw new RuntimeException($msg);
        }

        return $response['body']['data']['order']['fulfillmentOrders']['edges'];
    }

    /**
     * Gets open (OPEN or IN_PROGRESS) Fulfillment Order ID for order
     *
     * @param Order $order
     * @return string - fulfillment order id
     */
    public function getOpenFulfillmentOrderId(Order $order)
    {
        $query = '
          query getOrderByID($id: ID!) {
            order(id: $id) {
              id
              name
              fulfillmentOrders(first: 10) {
                edges {
                  node {
                    id
                    status
                  }
                }
              }
            }
          }
        ';

        $response = $this->api->graph($query, [
            'id' => 'gid://shopify/Order/' . $order->shopify_id
        ]);

        if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
            $msg = "Unable to get fulfillment orders from shopify: " . json_encode($response['errors']);
            \Log::error($msg);
            throw new RuntimeException($msg);
        }

        foreach ($response['body']['data']['order']['fulfillmentOrders']['edges'] as $edge) {
            if ($edge['node']['status'] == 'OPEN' || $edge['node']['status'] == 'IN_PROGRESS') {
                return $edge['node']['id'];
            }
        }

        throw new \RuntimeException('No open Fulfillment order found for order ' . $order->shopify_id);
    }


    /**
     * Gets an order not attribute value by its name from Shopify's order data
     *
     * @param String $attributeName
     * @param stdClass $orderData
     * @return string
     */
    protected function getNoteAttribute(String $attributeName, stdClass $orderData)
    {
        foreach ($orderData->note_attributes as $noteAttribute) {
            if ($noteAttribute->name === $attributeName) {
                return $noteAttribute->value;
            }
        }
        return '';
    }

    /**
     * Replaces product images based on the product SAP ID
     * Image name must follow the pattern: [SAP ID, no leading 0s].png
     */
    public function replaceProductImage(String $img, $baseUrl)
    {
        $imgSplit = explode('.', $img);
        $imgSplit = explode('-', $imgSplit[0]);
        $sapId = str_pad($imgSplit[0], 18, '0', STR_PAD_LEFT);
        $product = Product::where('sap_matnr', $sapId)->first();
        $baseUrl = substr($baseUrl, -1) == '/' ? $baseUrl : $baseUrl . '/';
        $position = sizeof($imgSplit) == 2 ? ($imgSplit[1] * 1 + 1) : 1;

        if (!$product) {
            \Log::warning('Image replace - Product ' . $sapId . ' not found!');
            return false;
        }
        if (!$product->shopify_id) {
            \Log::warning('Image replace - Product ' . $sapId . ' not on shopify!');
            return false;
        }
        $response = $this->api->rest('GET', '/admin/api/products/' . $product->shopify_id . '/images.json');

        if ($response['status'] !== 200) {
            \Log::error('Image replace - Unable to get product Images: ' . $sapId);
            return false;
        }

        if (!empty($response['body']['images'][$position - 1])) {
            // Deletes Product Image in position (it's a replace)
            $delResponse = $this->api->rest('DELETE', '/admin/api/products/' . $product->shopify_id . '/images/' . $response['body']['images'][$position - 1]['id'] . '.json');
            if ($delResponse['status'] !== 200) {
                \Log::error('Image replace - Unable to delete product image: ' . $sapId);
                return false;
            }
        }

        $createResponse = $this->api->rest('POST', '/admin/api/products/' . $product->shopify_id . '/images.json', [
            'json' => [
                'image' => [
                    'src' => $baseUrl . $img,
                    'position' => $position
                ]
            ]
        ]);

        if ($createResponse['status'] !== 200) {
            \Log::error('Image replace - Unable to add product main image: ' . $sapId);
            return false;
        }
        \Log::info('Image replace - SUCCESS: ' . $sapId);
        return true;
    }

    /**
     * Checks shopify for expired payment orders and cancels them if payment code (MB) has expired
     */
    public function checkExpiredOrders()
    {
        \Log::info('Checking payment expired open orders to cancel...');

        // Gets only orders placed more than 2 days ago (since condition is < yyyy-mm-dd, it's assumed that the time is 0:0:0)
        $refDate = Carbon::now()->subDays(1)->format('Y-m-d');
        $query = "
            {
                orders(first: 20, query: \"status:open AND financial_status:expired AND created_at:<$refDate\") {
                    edges {
                        node {
                            id,
                            displayFinancialStatus,
                            createdAt,
                            note,
                            lineItems(first: 20) {
                                edges {
                                    node {
                                        variant {
                                            id
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        ";

        $response = $this->api->graph($query);

        if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
            $msg = "Unable to get pending orders from shopify: " . json_encode($response['errors']);
            \Log::error($msg);
            throw new RuntimeException($msg);
        }

        $pendingOrders = $response['body']['data']['orders']['edges'];

        \Log::info('Found ' . sizeof($pendingOrders) . ' open order(s) with pending payment...');

        foreach ($pendingOrders as $orderNode) {
            $order = $orderNode['node'];
            $orderId = StringHelper::getIdFromGid($order['id']);

            $this->cancelOrder($orderId, 'other');

            // Synchronizes inventory for product
            foreach ($order['lineItems']['edges'] as $lineItemNode) {
                $lineItem = $lineItemNode['node'];
                if (empty($lineItem['variant']['id'])) {
                    continue;
                }

                $variantId = StringHelper::getIdFromGid($lineItem['variant']['id']);
                $product = Product::where('variant_id', $variantId)
                    ->orWhere('child_variant_id', $variantId)->first();

                if (!$product) {
                    \Log::warning('Order to cancel with product not found: shopify variant id - ' . $variantId);
                    continue;
                }

                // forces inventory related sync flags so that info is sent to Shopify
                if (isset($product->price->sync_data['toSync'])) {
                    $syncData = $product->price->sync_data;
                    $syncData['toSync']['quant'] = true;
                    $syncData['toSync']['avail'] = true;
                    $product->price->sync_data = $syncData;
                }
                $this->syncPrice($product->price);
            }
        }
    }

    /**
     * Cancels an order in shopify
     *
     * @param integer $orderId
     * @param string $reason
     * @param boolean $restock
     * @param boolean $notifyClient
     * @return void
     */
    public function cancelOrder(int $orderId, String $reason = 'other', bool $restock = false, bool $notifyClient = false)
    {
        $params = [
            'reason' => $reason,
            'email' => $notifyClient,
            'restock' => $restock
        ];

        $response = $this->api->rest('POST', '/admin/orders/' . $orderId . '/cancel.json', [
            'json' => $params
        ]);

        if ($response['status'] !== 200) {
            \Log::error('Unable to cancel shopify order ' . $orderId . ': ' .  serialize($response['body']));
            return false;
        }
        \Log::info('Order ' . $orderId . ' canceled');
    }

    /**
     * Copies source product content blocks metafields to target products so they have same editorial content
     *
     * @param integer $source
     * @param array $target
     * @return void
     */
    public function copyProductContents(int $source, array $target)
    {
        $contentFields = ['block_type', 'block_title', 'block_text', 'block_image_1', 'image_1_caption', 'block_image_2'];
        $sapId = str_pad($source, 18, "0", STR_PAD_LEFT);
        $product = Product::where('sap_matnr', $sapId)->first();
        if (!$product) {
            throw new RuntimeException('source product not found');
        }
        $shopifyProduct = $this->getProductMetafields($product->shopify_id);

        foreach ($target as $targetProductId) {
            $targetProductSapId = str_pad($targetProductId, 18, "0", STR_PAD_LEFT);
            $targetProduct = Product::where('sap_matnr', $targetProductSapId)->first();
            if (!$targetProduct || !$targetProduct->shopify_id) {
                throw new RuntimeException('target product not found: ' . $targetProductId);
            }
            $copiedKeys = [];
            foreach ($shopifyProduct['metafields']['edges'] as $metafieldNode) {
                $metafield = $metafieldNode['node'];
                if (in_array($metafield['key'], $contentFields)) {
                    $copiedKeys[] = $metafield['key'];
                    $this->upsertObjectMetafield('product', $targetProduct->shopify_id, $metafield['key'], $metafield['value'], 'json_string', 'area');
                }
            }

            // if no content metafield found, throws excepton
            if (sizeof($copiedKeys) === 0) {
                throw new RuntimeException('Origin product has no content!');
            }

            // clears remaing fields not present in origin
            if (sizeof($copiedKeys) < sizeof($contentFields)) {
                $toClear = array_diff($contentFields, $copiedKeys);
                foreach ($toClear as $toClearKey) {
                    $this->upsertObjectMetafield('product', $targetProduct->shopify_id, $toClearKey, '[]', 'json_string', 'area');
                }
            }
        }
    }

    /**
     * Tags an order in shopify
     *
     */
    public function addOrderTag(Order $order, string $tag)
    {
        $query = '
            mutation OrderTagsAdd($order_id: ID!, $order_tags: [String!]!) {
                tagsAdd(id: $order_id, tags: $order_tags) {
                    node {
                        id
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

        $response = $this->api->graph($query, [
            'order_id' => 'gid://shopify/Order/' . $order->shopify_id,
            'order_tags' => [$tag]
        ]);

        if (($response['status'] !== 200 && $response['status'] !== 201) || $response['errors']) {
            $msg = "Unable to add tag to order: " . json_encode($response['errors']);
            \Log::error($msg);
            throw new RuntimeException($msg);
        }

        return true;
    }

    public function testGiftCardAdjustments()
    {
        $response = $this->api->rest('POST', '/admin/gift_cards/503587504314/adjustments.json', [
            'json' => [
                'adjustment' => [
                    'amount' => -5
                ]
            ]
        ]);
        \Log::info(serialize($response));
    }

    public function testUserApi()
    {
        $response = $this->api->rest('GET', '/admin/users/current.json');
        \Log::info(json_encode($response['body']));
    }

    public function testCreateGiftCard()
    {
        $response = $this->api->rest('POST', '/admin/api/gift_cards.json', [
            'json' => [
                'gift_card' => [
                    'note' => "test gift card api",
                    'initial_value' => 100,
                    'code' => 'dddddddddd'
                ]
            ]
        ]);
        \Log::info(serialize($response));
    }

    public function checkChildProductsVariants()
    {
        $parentProducts = Product::where('is_parent', true)
            ->where('disabled', false)
            ->get();

        foreach($parentProducts as $parent) {
            if (!$parent->shopify_id) {
                continue;
            }
            $query =
                '{
                    product(id: "gid://shopify/Product/' . $parent->shopify_id . '"){
                        id
                        info: metafield(namespace: "eae", key: "info") {
                            id
                        }
                        translations: metafield(namespace: "eae", key: "translations") {
                            id
                        }
                        variants(first:30) {
                            edges {
                                node {
                                    id
                                    title
                                    inventoryItem {
                                        id
                                    }
                                    translations: metafield(namespace: "eae", key: "translations") {
                                        id
                                    }
                                }
                            }
                        }
                    }
                }';

            $response = $this->api->graph($query);
            $variants = $response['body']['data']['product']['variants']['edges'];

            foreach($variants as $variant) {
                $variant = $variant['node'];
                $variantId = StringHelper::getIdFromGid($variant['id']);

                $childProducts = Product::where('child_variant_id', $variantId)
                    ->get();

                if ($childProducts->count() > 1) {
                    \Log::info('MORE THAN ONE CHILD => ' . $parent->sap_matnr);
                }

                foreach($childProducts as $child) {
                    // \Log::info(substr($variant['title'], 0, 5) . ' -> ' . substr($child->sap_matnr, -5, 5));
                    $shopifyMatnr = substr($variant['title'], 0, 5);
                    $intMatnr = substr($child->sap_matnr, -5, 5);
                    if ($shopifyMatnr != $intMatnr) {
                        \Log::info('FOUND WRONG -> ' . $shopifyMatnr . ' -> ' . $intMatnr);
                    }
                }
            }
        }
    }
}
