<?php

namespace App\Services\Shopify;

use App\Helpers\StringHelper;
use App\Models\Catalog;
use App\Models\Product;
use App\Models\ProductCollection;
use App\Models\ProductMeta;
use App\Models\Variant;
use Illuminate\Support\Facades\Log;

/**
 * Service class that holds all product related Shopify related interaction and business logic
 */
class ProductsService extends BaseService
{
    /**
     * Syncs a given catalog with Shopify
     *
     * @param Catalog $catalog
     * @return bool
     */
    public function syncCatalog(Catalog $catalog)
    {
        $input = [
            'title' => $catalog->name,
            'status' => 'ACTIVE'
        ];

        if (!$catalog->shopify_gid) {
            $publicationData = $this->_createPublication($catalog->item_discount_group == '*');
            if (!$publicationData) {
                return false;
            }
            $input['publicationId'] = $publicationData['id'];

            $priceListData = $this->_createPriceList($catalog->name, $catalog->discount ? $catalog->discount : 0);
            if (!$priceListData) {
                return false;
            }
            $input['priceListId'] = $priceListData['id'];

            $query = '
                mutation catalogCreate($input: CatalogCreateInput!) {
                    catalogCreate(input: $input) {
                        catalog {
                            id
                            publication {
                                id
                            }
                            priceList {
                                id
                            }
                        }
                        userErrors {
                            field
                            message
                        }
                    }
                }
            ';

            $input['context'] = [
                'companyLocationIds' => [],
            ];


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

            if (!$this->_checkResponse($response, 'catalogCreate')) {
                return false;
            }

            $catalogResponse = $response['body']['data']['catalogCreate']['catalog'];
            $catalog->shopify_gid = $catalogResponse['id'];
            $catalog->publication_gid = !empty($catalogResponse['publication']['id']) ? $catalogResponse['publication']['id'] : null;
            $catalog->price_list_gid = !empty($catalogResponse['priceList']['id']) ? $catalogResponse['priceList']['id'] : null;
        } else {
            $query = '
                mutation catalogUpdate($id: ID!, $input: CatalogUpdateInput!) {
                    catalogUpdate(id: $id, input: $input) {
                        userErrors {
                            field
                            message
                        }
                    }
                }
            ';

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

            if (!$this->_checkResponse($response, 'catalogUpdate')) {
                return false;
            }

            if ($catalog->discount != null) {
                $this->_updatePriceList($catalog->price_list_gid, $catalog->discount);
            }
        }

        $catalog->to_sync = false;
        $catalog->save();
        return true;
    }

    /**
     * Syncs all pending Catalogs with Shopify
     *
     * @return void
     */
    public function syncCatalogs()
    {
        $catalogs = Catalog::where('to_sync', true)->get();

        Log::info('Found ' . $catalogs->count() . ' catalogs to sync with Shopify...');

        foreach ($catalogs as $catalog) {
            $this->syncCatalog($catalog);
        }
    }

    /**
     * Syncs all pending Products with Shopify
     *
     * @return void
     */
    public function syncProducts()
    {
        $products = Product::where('to_sync', true)->get();

        Log::info('Found ' . $products->count() . ' products to sync with Shopify...');

        foreach ($products as $product) {
            $this->syncProduct($product);
        }
    }

    /**
     * Syncs a given product with shopify (creating or updating previouly synced product)
     * Adds product to collections and related catalogs
     *
     * @param Product $product
     * @return bool
     */
    public function syncProduct(Product $product)
    {
        Log::info('Syncing Product ' . $product->nav_no . ' to Shopify...');

        $handle = StringHelper::urlize($product->title . ' ' . $product->nav_no);

        $shopifyProductInput = [
            'title' => $product->title,
            'handle' => $handle,
            'vendor' => 'Noldus',
            'metafields' => $this->_getProductMetafieldsInput($product),
            // 'variants' => $this->_getProductVariantsInput($product),
            // 'published' => true,
            'status' => $product->shopify_status
        ];

        $mutation = 'productCreate';
        $inputType = 'ProductCreateInput';
        if ($product->shopify_gid) {
            $mutation = 'productUpdate';
            $inputType = 'ProductUpdateInput';
            $shopifyProductInput['id'] = $product->shopify_gid;
        }

        foreach ($product->collections as $collection) {
            if ($collection->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_ADD) {
                if (empty($shopifyProductInput['collectionsToJoin'])) {
                    $shopifyProductInput['collectionsToJoin'] = [];
                }
                $shopifyProductInput['collectionsToJoin'][] = $collection->shopify_gid;
            } elseif ($collection->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_REMOVE) {
                if (empty($shopifyProductInput['collectionsToLeave'])) {
                    $shopifyProductInput['collectionsToLeave'] = [];
                }
                $shopifyProductInput['collectionsToLeave'][] = $collection->shopify_gid;
            }
        }

        $query = '
            mutation ' . $mutation . '($input: ' . $inputType . '!) {
                ' . $mutation . '(product: $input) {
                    product {
                        id
                        variants(first:20) {
                            edges {
                                node {
                                    id
                                    title
                                    inventoryItem {
                                        id
                                    }
                                }
                            }
                        }
                        metafields(first:10) {
                            edges {
                                node {
                                    id
                                    key
                                    namespace
                                }
                            }
                        }
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

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

        if (!$this->_checkResponse($response, $mutation)) {
            return false;
        }

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

        if ($mutation == 'productCreate') {
            $this->publishToOnlineStore($responseProduct['id']);
            $product->shopify_gid = $responseProduct['id'];
            $defaultVariant = $product->variants->first();
            $defaultVariant->shopify_gid = $responseProduct['variants']['edges'][0]['node']['id'];
            $defaultVariant->inventory_item_gid = $responseProduct['variants']['edges'][0]['node']['inventoryItem']['id'];
            $defaultVariant->save();
            $product->save();
        }

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

        // Update collections sync state
        $collectionsToSync = [];
        foreach ($product->collections as $collection) {
            if ($collection->pivot->status != Product::PRODUCT_RELATION_STATUS_TO_REMOVE) {
                $collectionsToSync[] = [
                    'collection_id' => $collection->id,
                    'status' => Product::PRODUCT_RELATION_STATUS_SYNCED
                ];
            }
        }
        $product->collections()->sync($collectionsToSync);

        /*
        foreach ($product->variants as $i => $variant) {
            $responseVariant = $responseProduct['variants']['edges'][$i]['node'];
            $variant->shopify_gid = $responseVariant['id'];
            $variant->inventory_item_gid = $responseVariant['inventoryItem']['id'];
            $variant->to_sync = false;
            $variant->save();
        }
        */

        foreach ($product->meta as $i => $meta) {
            $responseMeta = null;
            foreach ($responseProduct['metafields']['edges'] as $item) {
                if ($item['node']['namespace'] === $this->metaNamespace && $item['node']['key'] === $meta->key) {
                    $responseMeta = $item['node'];
                }
            }

            if ($responseMeta) {
                $meta->shopify_gid = $responseMeta['id'];
                $meta->save();
            }
        }

        if ($product->is_bundle && !$this->_updateBundle($product)) {
            Log::info('Error Creating product bundle components ' . $product->nav_no . ': ' . $product->shopify_gid);
            return false;
        }

        $syncCatalogSuccess = $this->_syncProductCatalogs($product);

        $product->to_sync = !$syncCatalogSuccess;
        $product->save();
        Log::info('Created / Updated shopify product ' . $product->nav_no . ': ' . $product->shopify_gid);

        return $syncCatalogSuccess;
    }

    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' => $product->shopify_gid,
            'variantsIds' => $variantGids
        ]);

        if (!$this->_checkResponse($response, 'productVariantsBulkDelete')) {
            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' => $product->shopify_gid,
            'variants' => $variants
        ]);

        if (!$this->_checkResponse($response, 'productVariantsBulkUpdate')) {
            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' => $product->shopify_gid,
            'variants' => $variants
        ]);

        if (!$this->_checkResponse($response, 'productVariantsBulkCreate')) {
            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->_getProductVariantsInput($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;
    }

    /**
     * Syncs all pending Variant Prices to Shopify
     *
     * @return void
     */
    public function syncPendingVariantPrices()
    {
        $variants = Variant::where('to_sync', true)
            ->get();

        foreach ($variants as $variant) {
            if ($this->syncVariantPrice($variant)) {
                $variant->to_sync = false;
                $variant->save();
            }
        }
    }

    /**
     * Syncs a variant price with shopify
     *
     * @param Variant $variant
     * @return bool
     */
    public function syncVariantPrice(Variant $variant)
    {
        if (!$variant->shopify_gid) {
            Log::warning('Unable to sync Variant price with Shopify - variant id: ' . $variant->id);
            return false;
        }

        if ($variant->is_bundle) {
            Log::warning('Unable to sync Variant price with Shopify - Bundle variant id: ' . $variant->id);
            return false;
        }

        $query = '
            mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
                productVariantsBulkUpdate(productId: $productId, variants: $variants) {
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

        $response = $this->api->graph($query, [
            'productId' => $variant->product->shopify_gid,
            'variants' => [
                [
                    'id' => $variant->shopify_gid,
                    'price' => $variant->price
                ]
            ]
        ]);

        if (!$this->_checkResponse($response, 'productVariantsBulkUpdate')) {
            return false;
        }

        Log::info('Updated shopify variant price ' . $variant->product->nav_no . ': ' . $variant->shopify_gid);
        return true;
    }

    /**
     * Syncs a product collection with shopify
     *
     * @param ProductCollection $collection
     * @return bool
     */
    public function syncCollection(ProductCollection $collection)
    {
        Log::info('Syncing Collection ' . $collection->nav_no . ' to Shopify...');

        $input = [
            'title' => $collection->title,
        ];

        $mutation = 'collectionCreate';
        if ($collection->shopify_gid) {
            $mutation = 'collectionUpdate';
            $input['id'] = $collection->shopify_gid;
        }

        $query = '
            mutation ' . $mutation . '($input: CollectionInput!) {
                ' . $mutation . '(input: $input) {
                    collection {
                        id
                    }
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

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

        if (!$this->_checkResponse($response, $mutation)) {
            return false;
        }

        $responseCollection = $response['body']['data'][$mutation]['collection'];
        $collection->shopify_gid = $responseCollection['id'];
        $collection->to_sync = false;
        $collection->save();

        Log::info('Created / Updated shopify collection ' . $collection->nav_no . ': ' . $collection->shopify_gid);

        if ($mutation == 'collectionCreate') {
            $this->publishToOnlineStore($collection->shopify_gid);
        }

        return true;
    }

    /**
     * Syncs all marked collections with Shopify
     *
     * @return void
     */
    public function syncCollections()
    {
        $collections = ProductCollection::where('to_sync', true)
            ->get();

        Log::info('Syncing Collections with Shopify... found ' . $collections->count());

        foreach ($collections as $collection) {
            $this->syncCollection($collection);
        }
    }

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

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

        if (!$this->_checkResponse($response, 'productUpdate')) {
            return false;
        }

        return true;
    }

    /**
     * Updates bundle product with current component configuration
     *
     * @param Product $product
     * @return bool
     */
    protected function _updateBundle(Product $product)
    {
        $variant = $product->variants->first();
        $variantRelationsToCreate = [];
        $variantRelationsToUpdate = [];

        // gets current variant relations
        $productResponse = $this->api->graph('
            query {
                productVariant(id: "' . $variant->shopify_gid . '") {
                    productVariantComponents(first:20) {
                        edges {
                            node {
                                productVariant {
                                    id
                                }
                            }
                        }
                    }
                }
            }
        ');
        $existingRelations = $productResponse['body']['data']['productVariant']['productVariantComponents']['edges'];

        $existingRelationIds = [];
        foreach ($existingRelations as $existingRelation) {
            $existingRelationIds[] = $existingRelation['node']['productVariant']['id'];
        }

        foreach ($variant->componentVariants as $componentVariant) {
            $existingKey = array_search($componentVariant->shopify_gid, $existingRelationIds);
            if ($existingKey !== false) {
                $variantRelationsToUpdate[] = [
                    'id' => $componentVariant->shopify_gid,
                    'quantity' => $componentVariant->pivot->quantity_per_item
                ];
                unset($existingRelationIds[$existingKey]);
            } else {
                $variantRelationsToCreate[] = [
                    'id' => $componentVariant->shopify_gid,
                    'quantity' => $componentVariant->pivot->quantity_per_item
                ];
            }
        }

        $priceInput = [];
        if (config('shopify-app.bundle_price_calculation') === 'fixed') {
            $priceInput = [
                'calculation' => 'FIXED',
                'price' => $variant->price ? $variant->price : 0
            ];
        } else {
            $priceInput = [
                'calculation' => 'COMPONENTS_SUM',
            ];
        }

        $input = [
            'parentProductId' => $product->shopify_gid,
            'parentProductVariantId' => $variant->shopify_gid,
            'priceInput' => $priceInput
        ];

        if (!empty($variantRelationsToUpdate)) {
            $input['productVariantRelationshipsToUpdate'] = $variantRelationsToUpdate;
        }

        if (!empty($variantRelationsToCreate)) {
            $input['productVariantRelationshipsToCreate'] = $variantRelationsToCreate;
        }

        if (!empty($existingRelationIds)) {
            $input['productVariantRelationshipsToRemove'] = array_values($existingRelationIds);
        }

        $query = '
            mutation productVariantRelationshipBulkUpdate($input: [ProductVariantRelationshipUpdateInput!]!) {
                productVariantRelationshipBulkUpdate(input: $input) {
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

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

        if (!$this->_checkResponse($response, 'productVariantRelationshipBulkUpdate')) {
            return false;
        }
        return true;
    }

    /**
     * Gets product variant input data to send to shopify
     *
     * @param Product $product
     * @return array - input variant data
     */
    protected function _getProductVariantsInput(Product $product)
    {
        $variantsInput = [];
        foreach ($product->variants as $variant) {
            $variantInput = [
                'optionValues' => [
                    'optionName' => 'Title',
                    'name' => $variant->title
                ],
                'price' => $variant->price == null && $variant->is_bundle ? 0 : $variant->price,
                'inventoryItem' => [
                    'sku' => $variant->sku,
                    'requiresShipping' => !$product->no_shipping,
                    'tracked' => false
                ],
            ];

            if (!$product->no_shipping && $variant->weight) {
                $variantInput['inventoryItem']['measurement']['weight']['value'] = $variant->weight;
                $variantInput['inventoryItem']['measurement']['weight']['unit'] = 'POUNDS';
            }

            if ($variant->shopify_gid) {
                $variantInput['id'] = $variant->shopify_gid;
            }

            $variantsInput[] = $variantInput;
        }

        return $variantsInput;
    }

    /**
     * Gets product metafield data to send to shopify
     *
     * @param Product $product
     * @return array - metafield data
     */
    protected function _getProductMetafieldsInput(Product $product)
    {
        if ($product->is_bundle) {
            $bundleMeta = $product->meta()->where('key', 'bundle')->first();
            if (!$bundleMeta) {
                $bundleMeta = new ProductMeta();
                $bundleMeta->key = 'bundle';
                $bundleMeta->type = 'list.variant_reference';
            }
            $gids = [];
            foreach ($product->variants->first()->componentVariants as $component) {
                $gids[] = "\"$component->shopify_gid\"";
            }
            $bundleMeta->value = '[' . implode(',', $gids) . ']';
            $product->meta()->save($bundleMeta);
        }

        $metafieldsInput = [];
        foreach ($product->meta as $meta) {
            $metafieldInput = [
                'key' => $meta->key,
                'namespace' => $this->metaNamespace,
                'type' => $meta->type,
                'value' => $meta->value
            ];

            if ($meta->shopify_gid) {
                $metafieldInput['id'] = $meta->shopify_gid;
            }

            $metafieldsInput[] = $metafieldInput;
        }

        return $metafieldsInput;
    }

    /**
     * Updates the discount on given existing shopify price list
     *
     * @param string $gid
     * @param integer $discount
     * @return bool
     */
    protected function _updatePriceList(string $gid, int $discount)
    {
        $query = '
            mutation priceListUpdate($id: ID!, $input: PriceListUpdateInput!) {
                priceListUpdate(id: $id, input: $input) {
                    userErrors {
                        field
                        message
                    }
                }
            }
        ';

        $response = $this->api->graph($query, [
            // TODO: replace currency for logical currency
            'id' => $gid,
            'input' => [
                'parent' => [
                    'adjustment' => [
                        'type' => 'PERCENTAGE_DECREASE',
                        'value' => $discount
                    ]
                ]
            ]
        ]);

        if (!$this->_checkResponse($response, 'priceListUpdate')) {
            return false;
        }

        return true;
    }

    /**
     * Syncs a product's catalog relationships with shopify
     *
     * @param Product $product
     * @return bool
     */
    protected function _syncProductCatalogs(Product $product)
    {
        $productCatalogs = $product->catalogs;
        $success = true;
        $toSync = [];
        foreach ($productCatalogs as $catalog) {
            if ($catalog->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_ADD) {
                $success = $success && $this->_updatePublication($catalog->publication_gid, [$product->shopify_gid]);
            } elseif ($catalog->pivot->status == Product::PRODUCT_RELATION_STATUS_TO_REMOVE) {
                $success = $success && $this->_updatePublication($catalog->publication_gid, [], [$product->shopify_gid]);
                continue;
            }

            $toSync[] = [
                'catalog_id' => $catalog->id,
                'status' => Product::PRODUCT_RELATION_STATUS_SYNCED
            ];
        }
        if (!$success) {
            Log::warning('Unable to sync product catalogs with shopify - ' . $product->nav_no . ' - ' . $product->id);
            return false;
        }
        $product->catalogs()->sync($toSync);
        return true;
    }
}
