<?php

namespace App\SAP;

use App\Jobs\UpdateOrderStatusJob;
use App\Models\Brand;
use App\Models\Category;
use App\Models\CategoryLang;
use App\Models\Customer;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Price;
use App\Models\Product;
use App\Models\ProductLang;
use App\Models\SyncControl;
use App\SAP\SAPClient;
use Carbon\Carbon;
use RuntimeException;
use stdClass;
use Illuminate\Support\Facades\DB;

/**
 * Service class that holds all SAP related interaction and business logic
 */
class SAPService
{
    const PRODUCTS_SYNC_CHUNK_SIZE = 400;

    const SYNC_TYPE_BRANDS = 'brands';
    const SYNC_TYPE_CATEGORIES = 'categories';
    const SYNC_TYPE_SUBCATEGORIES = 'subcategories';

    const LANG_MAP = ['E' => 'en', 'P' => 'pt'];

    const CUSTOMER_TYPES_MAP = [
        'B' => '0003',
        'P' => '0004'
    ];

    private $sapClient;
    private $brands;
    private $subCats;

    public function __construct(SAPClient $sapClient)
    {
        $this->sapClient = $sapClient;
    }

    /**
     * Complete Sync of product catalog from SAP (brands, collections, products and prices)
     *
     * @return void
     */
    public function syncCatalog()
    {
        $materials = $this->sapClient->getMaterials();

        $this->syncBrands($materials->TBrands->item);
        $this->syncCategories($materials->TCateg->item, $materials->TCategt->item, Category::CATEGORY_TYPE_CAT);
        $this->syncCategories($materials->TSubcat->item, $materials->TSubcatt->item, Category::CATEGORY_TYPE_SUBCAT);

        $this->syncProducts();
        $this->syncProducts(true);
    }

    /**
     * Syncs brands (creates / updates) based on SAP brands data
     *
     * @param array $sapBrands
     * @return void
     */
    protected function syncBrands($sapBrands)
    {
        \Log::info('Checking Brands to sync...');
        $syncControl = $this->getSyncControl(self::SYNC_TYPE_BRANDS);
        if ($this->checkSyncChanges($syncControl, $sapBrands)) {
            \Log::info('No changes since last sync!');
            return;
        }
        $brands = Brand::get()->keyBy('sap_brand');
        foreach ($sapBrands as $sapBrand) {
            if (empty($brands[$sapBrand->Brand])) {
                \Log::info('New brand: ' . $sapBrand->Brand);
                $brand = new Brand();
                $brand->sap_brand = $sapBrand->Brand;
            } else {
                $brand = $brands[$sapBrand->Brand];
            }
            if (!$this->checkSyncChanges($brand, $sapBrand)) {
                $brand->sap_descr = $sapBrand->Descr;
                $brand->save();
                \Log::info('Updated/Created Brand: ' . $sapBrand->Brand);

                // Marks products of this brand to sync
                Product::where('brand_id', $brand->id)
                    ->whereNull('parent_id')
                    ->where('disabled', false)
                    ->update(['to_sync' => true]);
            }
        }
        $syncControl->save();
    }

    /**
     * Syncs collections (categories) based on SAP data
     *
     * @param array $sapCats
     * @param array $sapCatsLang
     * @param string $type - Categories or sub categories
     * @return void
     */
    protected function syncCategories($sapCats, $sapCatsLang, $type)
    {
        \Log::info('Checking Categories  (' . $type . ') to sync...');
        $syncControl = $type === Category::CATEGORY_TYPE_CAT
            ? $this->getSyncControl(self::SYNC_TYPE_CATEGORIES)
            : $this->getSyncControl(self::SYNC_TYPE_SUBCATEGORIES);
        if ($this->checkSyncChanges($syncControl, [$sapCats, $sapCatsLang])) {
            \Log::info('No changes since last sync!');
            return;
        }

        $cats = Category::with('languages')
            ->where('type', $type)
            ->get()
            ->keyBy('sap_id');

        $idField = $type === Category::CATEGORY_TYPE_CAT ? 'Categ' : 'Subcat';
        foreach ($sapCats as $sapCat) {
            $sapId = $sapCat->$idField;
            if (empty($cats[$sapId])) {
                \Log::info('New Category (' . $type . '): ' . $sapCat->$idField);
                $category = new Category();
                $category->type = $type;
                $category->sap_id = $sapId;
            } else {
                $category = $cats[$sapId];
            }
            $langs = [];
            foreach ($sapCatsLang as $sapCatLang) {
                if ($sapCatLang->$idField === $sapCat->$idField) {
                    $langs[$sapCatLang->Spras] = $sapCatLang;
                }
            }
            if (!$this->checkSyncChanges($category, $sapCat, $langs)) {
                if ($type === Category::CATEGORY_TYPE_SUBCAT) {
                    $parentCat = Category::where('type', Category::CATEGORY_TYPE_CAT)
                        ->where('sap_id', $sapCat->Categ)
                        ->first();
                    $category->parent_id = $parentCat ? $parentCat->id : null;
                    $category->sap_order = $sapCat->Zorder;
                    $category->sap_gold = $sapCat->Gold;
                }

                $category->save();

                $syncCatProds = false;
                foreach ($langs as $lang => $sapCatLand) {
                    $catLang = $category->languages->findByLocale(self::LANG_MAP[$lang]);
                    if (!$catLang) {
                        $catLang = new CategoryLang();
                        $catLang->category_id = $category->id;
                        $catLang->language_id = self::LANG_MAP[$lang];
                    }

                    // If cat name changed, mark all cat products for update
                    if ($catLang->sap_descr != $sapCatLand->Descr){
                        $syncCatProds = true;
                    }
                    $catLang->sap_descr = $sapCatLand->Descr;
                    $catLang->save();
                }
                if ($syncCatProds) {
                    $idsToUpdate = [];
                    if ($type === Category::CATEGORY_TYPE_SUBCAT) {
                        $idsToUpdate[] = $category->id;
                    } else {
                        foreach ($category->categories as $childCat) {
                            $idsToUpdate[] = $childCat->id;
                        }
                    }
                    Product::whereIn('category_id', $idsToUpdate)
                        ->whereNull('parent_id')
                        ->where('disabled', false)
                        ->update(['to_sync' => true]);
                }
                \Log::info('Updated/Created Category (' . $type . '): ' . $sapCat->Categ);
            }
        }
        \Log::info('Done Category sync (' . $type . ')');
        $syncControl->save();
    }

    /**
     * Test function to check price changes - Doesn't sync - just logs changes
     *
     * @return void
     */
    public function checkPriceChanges()
    {
        \Log::info('Checking Products price changes (no sync)...');
        $sapIds = $this->sapClient->getProductIds();
        $chunks = array_chunk($sapIds, self::PRODUCTS_SYNC_CHUNK_SIZE);
        $startTime = microtime(true);
        $requestDuration = 0;
        foreach ($chunks as $chunk) {
            \Log::info('Processing chunck: ' . $chunk[0] . ' -> ' . last($chunk));
            $requestStartTime = microtime(true);
            $materials = $this->sapClient->getMaterials($chunk[0], last($chunk), true);
            $requestDuration +=  microtime(true) - $requestStartTime;
            $sapPrices = $materials->TPrices->item;
            $products = Product::with('price')
                ->whereIn('sap_matnr', $sapIds)
                ->get()
                ->keyBy('sap_matnr');
            foreach ($sapPrices as $sapPrice) {
                $price = null;
                if (empty($products[$sapPrice->Matnr])) {
                    \Log::info('Product does not exist: ' . $sapPrice->Matnr);
                    continue;
                }
                $product = $products[$sapPrice->Matnr];
                $price = null;
                if (empty($product->price)) {
                    $price = new Price();
                    $price->product_id = $product->id;
                    \Log::info('New price for product: ' . $sapPrice->Matnr);
                } else {
                    $price = $product->price;
                }
                if ($this->checkSyncChanges($price, $sapPrice)) {
                    \Log::info('No price change: ' . $sapPrice->Matnr);
                    continue;
                }
                $syncData = [];
                if (!empty($price->sync_data)) {
                    $syncData = $price->sync_data;
                }
                \Log::info('Changes Before ' . $sapPrice->Matnr . ': ' . str_pad($price->sap_price, 10)  . ' '
                    . str_pad($price->sap_price_prev, 10) . ' ' . $price->sap_price_type . ' '
                    . str_pad($price->sap_disc_perc, 4) . ' ' . str_pad($price->sap_disc_start, 10) . ' '
                    . str_pad($price->sap_disc_end, 10) . ' ' . $price->sap_avail . ' ' . str_pad($price->sap_lead_time, 5)
                    . ' ' . str_pad($price->sap_quant, 5));
                \Log::info('Changes AFTER  ' . $sapPrice->Matnr . ': ' . str_pad($sapPrice->Price, 10)  . ' '
                    . str_pad($sapPrice->PricePrev, 10) . ' ' . $sapPrice->PriceType . ' '
                    . str_pad($sapPrice->DiscPerc, 4) . ' ' . str_pad($sapPrice->DiscStart, 10) . ' '
                    . str_pad($sapPrice->DiscEnd, 10) . ' ' . $sapPrice->Avail . ' ' . str_pad($sapPrice->LeadTime, 5)
                    . ' ' . str_pad($sapPrice->Quant, 5));

                $price->sap_price = $sapPrice->Price;
                $price->sap_price_type = $sapPrice->PriceType * 1;
                $price->sap_price_prev = $sapPrice->PricePrev;
                $price->sap_disc_perc = $sapPrice->DiscPerc;
                $price->sap_disc_start = $sapPrice->DiscStart !== '0000-00-00' ? $sapPrice->DiscStart : null;
                $price->sap_disc_end = $sapPrice->DiscEnd !== '0000-00-00' ? $sapPrice->DiscEnd : null;
                $price->sap_avail = $sapPrice->Avail;
                $price->sap_lead_time = $sapPrice->LeadTime;
                $price->sap_quant = $sapPrice->Quant;

                $priceInfoToSync = $price->isDirty('sap_price_type') || $price->isDirty('sap_disc_start')
                    || $price->isDirty('sap_disc_end') || $price->isDirty('sap_lead_time');
                \Log::info(
                    $price->isDirty('sap_price_type') . ' '
                    . $price->isDirty('sap_disc_start') . ' '
                    . $price->isDirty('sap_disc_end') . ' '
                    . $price->isDirty('sap_lead_time')
                );
                $syncData['toSync'] = [
                    'price' => $price->isDirty('sap_price') || $price->isDirty('sap_price_prev') || !empty($syncData['toSync']['price']),
                    'quant' => $price->isDirty('sap_quant') || !empty($syncData['toSync']['quant']),
                    'avail' => $price->isDirty('sap_avail') || $price->isDirty('sap_price_type') || !empty($syncData['toSync']['avail']),
                    'info' => $priceInfoToSync || !empty($syncData['toSync']['info'])
                ];
                \Log::info('TO sync: ' . json_encode($syncData['toSync']));
            }

            \Log::info('End chunck: ' . $chunk[0] . ' -> ' . last($chunk));
        }
        \Log::info('Price changes check complete. SAP requests: ' . $requestDuration . ' Total: ' . (microtime(true) - $startTime));
    }

    /**
     * Compares returned products from SAP with existing integration ones to mark for disable the missing ones
     *
     * @param Array $sapIds
     * @return void
     */
    public function checkMissingProducts(Array $sapIds)
    {
        if (sizeof($sapIds) < 3000) { // Safety net
            \Log::warning("Skipping product deactivation sync... too few Products from SAP: " . sizeof($sapIds));
            return false;
        }
        $products = Product::whereNotIn('sap_matnr', $sapIds)
            ->where('to_disable', false)
            ->where('disabled', false)
            ->get()
            ->keyBy('sap_matnr');
        \Log::info("Found " . sizeof($products) . " missing products from SAP to disable (" . sizeof($sapIds) . ")");
        if (sizeof($products) === 0) {
            return;
        }
        $logToDisableIds = 'Product marked to disable: ';
        foreach ($products as $product) {
            $logToDisableIds .= intval($product->sap_matnr) . ',';
            $product->to_disable = true;
            $product->to_enable = false;
            $product->save();

            if ($product->parent) {
                $product->parent->to_sync = 1;
                $product->parent->save();
            }
        }
        \Log::info($logToDisableIds);
    }

    /**
     * Utility function that gets a specific product from sap and returs it's data
     *
     * @param [type] $sapMatnr
     * @return array - product & price data
     */
    public function getSapProductData($sapMatnr)
    {
        $materials = $this->sapClient->getMaterials($sapMatnr, $sapMatnr);
        if (!empty($materials->TMaterials->item)) {
            return [
                'material' => $materials->TMaterials->item,
                'price' => $materials->TPrices->item
            ];
        } else {
            return [
                'material' => null,
                'price' => null
            ];
        }
    }

    /**
     * Syncs a single product from SAP
     *
     * @param string $sapId
     * @return void
     */
    public function syncSingleProduct(string $sapId)
    {
        $sapId = str_pad($sapId, 18, '0', STR_PAD_LEFT);
        $materials = $this->sapClient->getMaterials($sapId, $sapId);
        if (empty($materials->TMaterials->item)) {
            throw new RuntimeException('Product not found!');
        }
        $sapProductData = $materials->TMaterials->item;
        $sapPriceData = $materials->TPrices->item;

        $product = Product::where('sap_matnr', $sapProductData->Matnr)
            ->first();

        $product = $this->syncProduct($sapProductData, $product, true);

        if (!$product) {
            throw new RuntimeException('Error Syncing product!');
        }

        $this->syncPrice($sapPriceData, $product, true);

        return true;
    }

    /**
     * Syncs products or prices from SAP (all)
     *
     * @param boolean $syncPrices - whether to sync product data or prices data
     * @return void
     */
    public function syncProducts($syncPrices = false)
    {
        \Log::info('Checking Products to sync...');
        $sapIds = $this->sapClient->getProductIds();
        $this->checkMissingProducts($sapIds);

        $chunks = array_chunk($sapIds, self::PRODUCTS_SYNC_CHUNK_SIZE);
        $startTime = microtime(true);
        $requestDuration = 0;
        foreach ($chunks as $chunk) {
            \Log::info('Processing chunck: ' . $chunk[0] . ' -> ' . last($chunk));
            $requestStartTime = microtime(true);
            $materials = $this->sapClient->getMaterials($chunk[0], last($chunk), $syncPrices);
            $requestDuration +=  microtime(true) - $requestStartTime;
            if (!$syncPrices) {
                $this->syncProductsChunk($chunk, $materials->TMaterials->item);
            } else {
                $this->syncPricesChunk($chunk, $materials->TPrices->item);
            }
            \Log::info('End chunck: ' . $chunk[0] . ' -> ' . last($chunk));
        }
        \Log::info('Products sync end. SAP requests: ' . $requestDuration . ' Total: ' . (microtime(true) - $startTime));
    }

    /**
     * Sync a specific product's price data from SAP
     *
     * @param [type] $sapId
     * @return void
     */
    public function syncProductPrice($sapId)
    {
        $material = $this->sapClient->getMaterials($sapId, $sapId, true);
        $this->syncPricesChunk([$sapId], [$material->TPrices->item]);
    }

    /**
     * Syncs the given product price data from SAP with integration db
     *
     * @param array $sapIds - Ids to sync
     * @param array $sapPrices - Prices' data for the given ids
     * @return void
     */
    protected function syncPricesChunk($sapIds, $sapPrices)
    {
        $products = Product::with('price')
            ->whereIn('sap_matnr', $sapIds)
            ->get()
            ->keyBy('sap_matnr');

        foreach ($sapPrices as $sapPrice) {
            if (empty($products[$sapPrice->Matnr])) {
                \Log::info('Price update fail! Product does not exist: ' . $sapPrice->Matnr);
                continue;
            }
            $this->syncPrice($sapPrice, $products[$sapPrice->Matnr]);
        }
    }

    /**
     * Syncs product SAP data
     *
     * @param Object $sapPrice
     * @param Product $product
     * @return void
     */
    protected function syncPrice(Object $sapPrice, Product $product, bool $force = false)
    {
        $price = null;
        if (empty($product->price)) {
            $price = new Price();
            $price->product_id = $product->id;
            \Log::info('New price for product: ' . $sapPrice->Matnr);
        } else {
            $price = $product->price;
        }
        if (!$force && $this->checkSyncChanges($price, $sapPrice)) {
            return;
        }

        \Log::info('Updating price (price / quant): old - ' . $price->sap_price . ' / ' . $price->sap_quant
            . ' new - ' . $sapPrice->Price . ' / ' . $sapPrice->Quant);
        $price->sap_price = $sapPrice->Price;
        $price->sap_price_type = $sapPrice->PriceType * 1;
        $price->sap_price_prev = $sapPrice->PricePrev;
        $price->sap_disc_perc = $sapPrice->DiscPerc;
        $price->sap_disc_start = $sapPrice->DiscStart !== '0000-00-00' ? $sapPrice->DiscStart : null;
        $price->sap_disc_end = $sapPrice->DiscEnd !== '0000-00-00' ? $sapPrice->DiscEnd : null;
        $price->sap_avail = $sapPrice->Avail;
        $price->sap_lead_time = $sapPrice->LeadTime;
        $price->sap_quant = $sapPrice->Quant;
        $price->to_sync = true;

        $syncData = [];
        if (!empty($price->sync_data)) {
            $syncData = $price->sync_data;
        }
        $priceInfoToSync = $price->isDirty('sap_price_type') || $price->isDirty('sap_disc_start')
            || $price->isDirty('sap_disc_end') || $price->isDirty('sap_lead_time');
        $syncData['toSync'] = [
            'price' => $price->isDirty('sap_price') || $price->isDirty('sap_price_prev') || !empty($syncData['toSync']['price']),
            'quant' => $price->isDirty('sap_quant') || $price->isDirty('sap_avail') || !empty($syncData['toSync']['quant']),
            'avail' => $price->isDirty('sap_avail') || $priceInfoToSync || !empty($syncData['toSync']['avail']),
            'info' => $priceInfoToSync || !empty($syncData['toSync']['info'])
        ];
        $price->sync_data = $syncData;

        if ($product->parent && $product->parent->price && $syncData['toSync']['avail']) {
            $parentPrice = $product->parent->price;
            $parentPrice->to_sync = true;
            $parentPrice->save();
        }

        $price->save();
        \Log::info('Product Price Updated/Created: ' . $sapPrice->Matnr);
        return true;
    }

    /**
     * Syncs the given products' data from SAP with integration db
     *
     * @param array $sapIds
     * @param array $sapProducts - Sap products' data
     * @return void
     */
    protected function syncProductsChunk($sapIds, $sapProducts)
    {
        $products = Product::with('languages')
            ->whereIn('sap_matnr', $sapIds)
            ->get()
            ->keyBy('sap_matnr');

        foreach ($sapProducts as $sapProduct) {
            $existingProduct = $products[$sapProduct->Matnr] ?? null;
            $this->syncProduct($sapProduct, $existingProduct);
        }
    }

    /**
     * Syncs a given SAP product DATA
     *
     * @return Product
     */
    protected function syncProduct(Object $sapProduct, Product $product = null, bool $force = false)
    {
        if (!$this->brands) {
            $this->brands = Brand::get()
                ->keyBy('sap_brand');
        }
        if (!$this->subCats) {
            $this->subCats = Category::where('type', Category::CATEGORY_TYPE_SUBCAT)
                ->get()
                ->keyBy('sap_id');
        }

        DB::beginTransaction();
        try {
            if (!$product) {
                $product = new Product();
                $product->sap_matnr = $sapProduct->Matnr;
                \Log::info('New Product: ' . $sapProduct->Matnr);
            } else {
                if ($product->to_disable && !$product->disabled) {
                    $product->to_disable = false;
                    $product->save();
                }
                if ($product->disabled) {
                    // If product already exists but was disabled, this is a reactivation so forces sync.
                    $product->sync_hash = '';
                    $product->to_enable = true;
                    $product->disabled = false;
                    $product->save();

                    if ($product->category) {
                        $product->category->to_sync = true;
                        $product->category->save();
                    }

                    if ($product->parent) {
                        $product->parent->to_sync = 1;
                        $product->parent->save();
                    }
                }
            }
            $langs = ['Pt', 'En'];
            $langFields = ['Descr', 'Specs', 'Dimen'];

            $catId = null;
            // Cat assoc
            if (!empty($this->subCats[$sapProduct->Subcat])) {
                $catId = $this->subCats[$sapProduct->Subcat]->id;
            }

            if ($catId !== $product->category_id) {
                // Marks before and after cat to be synced since its items changed
                if ($product->category) {
                    $product->category->to_sync = true;
                    $product->category->save();
                }
                if ($catId && !empty($this->subCats[$sapProduct->Subcat])) {
                    $this->subCats[$sapProduct->Subcat]->to_sync = true;
                    $this->subCats[$sapProduct->Subcat]->save();
                }

                $product->category_id = $catId;
                $product->to_sync = true; // must be in order to update tags
                $product->save();
            }

            if (!$force && $this->checkSyncChangesProduct($product, $sapProduct)) {
                DB::commit();
                return;
            }
            $product->to_sync = true;
            $product->sap_bstat = $sapProduct->Bstat;
            $product->sap_saiso = $sapProduct->Saiso;
            $product->sap_range = $sapProduct->Range;
            $product->sap_store = $sapProduct->Store;
            $product->sap_tags = $sapProduct->Tags;

            // Child / Parent Products
            if ($sapProduct->Grouph === 'X') {
                // Parent product
                $product->is_parent = true;
            } else if ($sapProduct->Groupi) {
                // Child product
                $parentProduct = Product::where('sap_matnr', $sapProduct->Groupi)
                    ->where('is_parent', true)
                    ->first();

                $product->child_order = $sapProduct->Groupin;
                $product->is_parent = false;

                if ($parentProduct) {
                    $product->parent_id = $parentProduct->id;
                    $parentProduct->to_sync = 1;
                    $parentProduct->save();
                    if ($product->price) {
                        $product->price->sync_data = null;
                        $product->price->to_sync = 1;
                        $product->price->save();
                    }
                } else {
                    \Log::warning('Product ' . $sapProduct->Matnr . ' is child but parent ' . $sapProduct->Groupi . ' not found');
                    DB::rollBack();
                    return;
                }
            } else {
                // Can be and old group product
                $product->is_parent = false;
                // Regular product - Can be an ex-child product
                if ($product->parent_id) {
                    $product->parent->to_sync = 1;
                    $product->parent->save();
                    $product->parent_id = null;
                    $product->to_enable = true;
                    if ($product->price) {
                        $product->price->sync_data = null;
                        $product->price->to_sync = 1;
                        $product->price->save();
                    }
                }
            }

            // Brand assoc
            if (empty($this->brands[$sapProduct->Brand])) {
                $product->brand_id = null;
            } else {
                $product->brand_id = $this->brands[$sapProduct->Brand]->id;
            }

            $product->save();

            // Langs
            foreach ($langs as $lang) {
                $productLang = $product->languages->findByLocale(strtolower($lang));
                if (!$productLang) {
                    $productLang = new ProductLang();
                    $productLang->product_id = $product->id;
                    $productLang->language_id = strtolower($lang);
                }
                foreach ($langFields as $langField) {
                    $fieldNameSap = $langField . $lang;
                    $fieldName = 'sap_' . strtolower($langField);
                    $productLang->$fieldName = $sapProduct->$fieldNameSap;
                }
                $productLang->save();
            }

            DB::commit();
            \Log::info('Product Updated/Created: ' . $sapProduct->Matnr);
            return $product;
        } catch (\Throwable $e) {
            \Log::error('Product NOT Updated/Created: ' . $sapProduct->Matnr);
            \Log::error($e);
            DB::rollBack();
        }
    }

    /**
     * Creates a customer in SAP - not used anymore
     *
     * @param Customer $customer
     * @return void
     */
    public function createSapCustomer(Customer $customer)
    {
        $sapCustomer = new stdClass();
        $address = $customer->addresses()->where('is_default', true)->first();

        $sapCustomer->CustNr = $customer->shopify_id;
        $sapCustomer->Zstore = config('sap.store_id');
        $sapCustomer->Ztitle = '0004'; // TODO: find a logic to set customer type
        $sapCustomer->Zname = mb_substr($customer->first_name . ' ' . $customer->last_name, 0, 70, 'utf-8');

        $sapCustomer->Zstreet = $address ? mb_substr($address->address1, 0, 40, 'utf-8') : '';
        $sapCustomer->Zstreet2 = $address ? mb_substr($address->address2, 0, 40, 'utf-8') : '';
        $sapCustomer->Zpcode = $address ? mb_substr($address->zip, 0, 10, 'utf-8') : '';
        $sapCustomer->Zcity = $address ? mb_substr($address->city, 0, 40, 'utf-8') : '';
        $sapCustomer->Zcountry = $address ? $address->country_code : null;
        $sapCustomer->Zsearch = $address ? mb_substr(str_replace(' ', '', $address->phone), 0, 20, 'utf-8') : '';
        $sapCustomer->Ztelf1 = '';
        $sapCustomer->Ztelf2 = '';

        $sapCustomer->Zemail = $customer->email;
        $sapCustomer->Zvat = ''; // TODO: find where this will come from
        $sapCustomer->Zcclass = $customer->accepts_marketing ? '01' : '02';

        $sapNr = $this->sapClient->createCustomer($sapCustomer);

        if (!$sapNr) {
            throw new RuntimeException('SAP create customer failed: no SAP id');
        }

        $customer->sap_cust_nr = $sapNr;
        $customer->to_sync = false;
        $customer->save();
    }

    /**
     * Sends a given order to SAP
     *
     * @param Order $order
     * @return void
     */
    public function createSapOrder(Order $order)
    {
        if ($order->financial_status != 'paid') {
            throw new RuntimeException('SAP create order failed: Order is not paid');
        }
        $sapOrder = new stdClass();

        $sapOrder->OrderNr = $order->shopify_id;
        $sapOrder->Zstore = config('sap.store_id');
        $sapOrder->CustNr = $order->customer->shopify_id;
        $sapOrder->StoreId = $order->order_number;

        // \Log::info('Sending customer to SAP: ' . json_encode($order->customer));
        // $this->createSapCustomer($order->customer);

        $billingAddress = $this->addressCropAndMerge($order->billing_address1, $order->billing_address2);
        $shippingAddress = $this->addressCropAndMerge($order->shipping_address1, $order->shipping_address2);

        $sapOrder->Zdate = $order->order_created_at->format('Y-m-d');
        $sapOrder->Zemail = $order->email;
        $sapOrder->Ztitle = !empty(self::CUSTOMER_TYPES_MAP[$order->customer_type]) ? self::CUSTOMER_TYPES_MAP[$order->customer_type] : '';
        $sapOrder->Name1F = mb_substr($order->billing_name, 0, 70, 'utf-8');
        $sapOrder->ZstreetF = $billingAddress[0];
        $sapOrder->Zstreet2F = $billingAddress[1];
        $sapOrder->ZpcodeF = mb_substr($order->billing_zip, 0, 10, 'utf-8');
        $sapOrder->ZcityF = mb_substr($order->billing_city, 0, 40, 'utf-8');
        $sapOrder->ZcountryF = $order->billing_country_code;
        $sapOrder->ZsearchF = mb_substr(str_replace(' ', '', $order->billing_phone), 0, 20, 'utf-8');
        $sapOrder->Zvat = mb_substr($order->tax_id, 0, 20, 'utf-8');

        $sapOrder->Name1 = $order->same_address ? '' : mb_substr($order->shipping_name, 0, 70, 'utf-8');
        $sapOrder->Zstreet = $order->same_address ? '' : $shippingAddress[0];
        $sapOrder->Zstreet2 = $order->same_address ? '' : $shippingAddress[1];
        $sapOrder->Zpcode = $order->same_address ? '' : mb_substr($order->shipping_zip, 0, 10, 'utf-8');
        $sapOrder->Zcity = $order->same_address ? '' : mb_substr($order->shipping_city, 0, 40, 'utf-8');
        $sapOrder->Zcountry = $order->same_address ? '' : $order->shipping_country_code;
        $sapOrder->Zsearch = $order->same_address ? '' : mb_substr(str_replace(' ', '', $order->shipping_phone), 0, 20, 'utf-8');
        $sapOrder->Ztelf1 = '';

        $sapOrder->Zcclass = $order->accepts_marketing ? '01' : '02';

        $sapOrder->Shipping = $order->shipping_total;
        $sapOrder->ShipDisc = $order->shipping_discount;
        $sapOrder->ShipDesc = mb_substr($order->shipping_type, 0, 40, 'utf-8');

        $sapOrder->PaymType = 'Card'; //TODO: change this to something meaningful
        $sapOrder->Amount = $order->total;

        $sapItems = [];
        foreach ($order->orderItems as $item) {
            $sapItem = new stdClass();
            $sapItem->Posnr = $item->pos;
            $sapItem->Matnr = $item->sap_matnr;
            $sapItem->Quant = $item->quantity;
            $sapItem->Amount = $item->total;
            $sapItem->Discount = $item->discount;
            $sapItem->Lfdat = $item->expected_date ? $item->expected_date->format('Y-m-d') : '';
            $sapItem->ShopItem = $item->shopify_id;
            $sapItems[] = $sapItem;
        }
        \Log::info('Sending order to SAP: ' . json_encode($sapOrder) . ' Items: ' . json_encode($sapItems));
        $sapNr = $this->sapClient->createOrder($sapOrder, $sapItems);

        if (!$sapNr) {
            throw new RuntimeException('SAP create order failed: no SAP doc id');
        }
        \Log::info('Order sent to SAP with success. SAP ref doc: ' . $sapNr);
        $order->to_sync = false;
        $order->save();
    }

    /**
     * Utility function to merge and optimize address data
     *
     * @param [type] $line1
     * @param [type] $line2
     * @return array
     */
    protected function addressCropAndMerge($line1, $line2)
    {
        $outlines = [
            "",
            ""
        ];
        if (mb_strlen($line1, 'utf-8') > 40) {
            $mergedlines = $line1 . ' ' . $line2;
            $words = explode(' ', $mergedlines);
            $lineIdx = 0;
            foreach ($words as $i => $word) {
                if ($lineIdx === 0 && mb_strlen($outlines[0] . ' ' . $word, 'utf-8') > 40) {
                    $lineIdx = 1;
                }
                $outlines[$lineIdx] .= (!$outlines[$lineIdx] ? '' : ' ') . $word;
            }
        } else {
            $outlines[0] = $line1;
            $outlines[1] = $line2;
        }
        $outlines[0] = mb_substr($outlines[0], 0, 40, 'utf-8');
        $outlines[1] = mb_substr($outlines[1], 0, 40, 'utf-8');
        return $outlines;
    }

    /**
     * Gets order status updates from SAP and updates integration orders for Shopify sync
     *
     * @return void
     */
    public function checkOrdersStatus()
    {
        \Log::info('Checking order status changes...');
        $syncControl = $this->getSyncControl('orders');
        if (!$syncControl->last_sync_at) {
            // If no last sync, defaults to dev date
            $syncControl->last_sync_at = Carbon::create(2021, 7, 1, 0, 0, 0, 'UTC');
        }
        $newSyncDate = Carbon::now();
        $statusLines = $this->sapClient->getOrdersStatus($syncControl->last_sync_at);
        \Log::info('SAP order status data: ' . json_encode($statusLines));

        $orders = $this->proccessSAPStausLines($statusLines);
        \Log::info('Found ' . sizeof($orders) . ' to update...');

        foreach ($orders as $orderId => $order) {
            $this->updateOrderStatus($orderId, $order);
        }

        $syncControl->last_sync_at = $newSyncDate;
        $syncControl->save();
        \Log::info('Order status update complete.');
    }

    /**
     * Proccess incoming SAP order status lines organizing them by order
     *
     * @param Array Status Lines
     * @return Array Orders status
     */
    protected function proccessSAPStausLines($statusLines)
    {
        $orders = [];
        foreach ($statusLines as $line) {
            if (empty($orders[$line->ShopOrder])) {
                $orders[$line->ShopOrder] = [
                    'lines' => [],
                    'order' => null
                ];
            }
            if ($line->SapItem == '000000') {
                $orders[$line->ShopOrder]['order'] = $line;
            } else {
                $orders[$line->ShopOrder]['lines'][$line->ShopItem] = $line;
            }
        }
        return $orders;
    }

    /**
     * Gets order status from SAP and syncs
     *
     * @param Order $order
     * @return void
     */
    public function checkOrderStatus(Order $order)
    {
        $statusLines = $this->sapClient->getOrderStatus($order->shopify_id);
        $ordersStatus = $this->proccessSAPStausLines($statusLines);
        if (sizeof($ordersStatus) !== 1) {
            throw new RuntimeException('No status from SAP (or more than one returned)!');
        }
        $orderStatus = array_values($ordersStatus)[0];
        $orderId = array_keys($ordersStatus)[0];

        return $this->updateOrderStatus($orderId, $orderStatus);
    }

    /**
     * Updates and marks for sync a given order based on SAP status update data
     *
     * @param [type] $orderId
     * @param Array $orderStatus
     * @return Order
     */
    protected function updateOrderStatus($orderId, Array $orderStatus)
    {
        \Log::info('Updating status for order ' . $orderId . '...');
        $order = Order::where('shopify_id', $orderId)->first();
        $toSyncStatus = false;
        if (!$order) {
            \Log::warning('Order ' . $orderId . ' not found!');
            return;
        }
        if (!empty($orderStatus['order'])) {
            $order->invoice_shipping = $orderStatus['order']->AmountInv;
            $order->shipping_to_refund = $orderStatus['order']->AmountRet - $order->refunded_shipping;
            $order->sap_order_nr = $orderStatus['order']->SapOrder;
            $order->no_transaction = $orderStatus['order']->RetPayment !== 'X';

            if ($order->shipping_to_refund > 0) {
                $toSyncStatus = true;
            }
        }
        foreach ($orderStatus['lines'] as $lineId => $line) {
            $item = OrderItem::where('order_id', $order->id)
                ->where('shopify_id', $lineId)
                ->first();

            if (!$item) {
                \Log::warning('Order item ' . $lineId . ' not found!');
                continue;
            }

            $item->quantity_to_refund = $line->QuantRet - $item->refunded_quantity;
            $item->quantity_to_fulfill = $line->QuantDel - $item->fulfilled_quantity;
            $item->invoice_quantity = $line->QuantInv;
            $item->invoice_total = $line->AmountInv;
            $item->refunded_total = $line->AmountRet;
            $item->no_transaction = $line->RetPayment !== 'X';

            if ($item->quantity_to_refund > 0 || $item->quantity_to_fulfill > 0) {
                $toSyncStatus = true;
            }
            $item->save();
        }
        $order->to_sync_status = $toSyncStatus;
        $order->save();

        if ($toSyncStatus) {
            UpdateOrderStatusJob::dispatch($order);
        }

        return $order;
    }

    /**
     * Checks a syncable object for changes since last sync
     * Updates syncControl object if change detected
     *
     * @return boolean - true if no changes detected.
     */
    protected function checkSyncChanges($syncControl, $object, $objectLangs = null)
    {
        if ($objectLangs) {
            $object = [$object, $objectLangs];
        }

        $currentHash = md5(serialize($object));
        if ($currentHash === $syncControl->sync_hash) {
            return true;
        }

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

    /**
     * Checks and generates a signature to control product data updates
     *
     * @param [type] $product
     * @param [type] $sapProduct
     * @return void
     */
    protected function checkSyncChangesProduct($product, $sapProduct)
    {
        $signature = [];

        $signature[] = $sapProduct->Bstat;
        $signature[] = $sapProduct->Saiso;
        $signature[] = $sapProduct->Range;
        $signature[] = $sapProduct->Brand;
        $signature[] = $sapProduct->DescrPt;
        $signature[] = $sapProduct->DescrEn;
        $signature[] = $sapProduct->SpecsPt;
        $signature[] = $sapProduct->SpecsEn;
        $signature[] = $sapProduct->DimenPt;
        $signature[] = $sapProduct->DimenEn;
        $signature[] = $sapProduct->Store;
        $signature[] = $sapProduct->Grouph;
        $signature[] = $sapProduct->Groupi;
        if ($sapProduct->Groupi) {
            $signature[] = $sapProduct->Groupin;
        }
        $signature[] = $sapProduct->Subcat;

        // Added fields to signature will need if to avoid syncing all products again
        if ($sapProduct->Tags) {
            $signature[] = $sapProduct->Tags;
        }

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

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

    /**
     * Gets a sync control for a give type
     *
     * @param string $type
     * @return SyncControl
     */
    protected function getSyncControl($type)
    {
        $syncControl = SyncControl::where('type', $type)->first();
        if (!$syncControl) {
            $syncControl = new SyncControl();
            $syncControl->type = $type;
        }
        return $syncControl;
    }
}
