MyDataProvider » Blog » Ikea.com country universal scraper – c# code sample

Ikea.com country universal scraper – c# code sample

  • by

Here is a sample of release code for ikea web scraper.
This code developed using our internal library for c# but you can adjust it for your case without using our internal dll.
Just replace data loading function with standart .net webclient or web request.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Web;
using CatalogLoaderCommon;
using HtmlAgilityPack;
using System.Linq;
using HtmlDocument = HtmlAgilityPack.HtmlDocument;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net;
using RestSharp.Extensions;

#if NET462
using Microsoft.CSharp; // for dynamics
#endif

namespace CatalogLoader._MyWebScrapers
{
    public class ikea_scraper : CustomScriptBase
    {
        public HtmlPageLoader HplCatalog { get; set; }
        private Product _product;
        private Category _category;
        private TaskInfo _mti;

        public ikea_scraper() { }

        internal enum Region
        {
            EN, PL, RU
        }

        internal sealed class IkeaSettings
        {
            public RegionSettings RegionSettings { get; set; }
            public Region Region { get; set; }
            public List<string> StoreIDs { get; set; }
            public int ProductQtyMin { get; set; }
            public double ProductPriceMin { get; set; }
            public bool CatalogModeEnabled { get; set; }
            public bool PackagesWeightTotalAsAttribute { get; set; }
            public bool ProductSizesAsPackagesSizesTotal { get; set; }
        }
        internal sealed class RegionSettings
        {
            public string Domain { get; set; }
            public string RootCategoryUrl { get; set; }
            public string ShopAllDetectorString { get; set; }
            public string ProductAvailabilityUrlTemplate { get; set; }
            public string MoreProductsUrl { get; set; }
        }

        public override void GrabCatalogBuild(GrabCatalogBuildScriptParameters p)
        {
            //Init(p);
            _category = new Category() { ID = "0", SourceUrl = _ikeaSettings.RegionSettings.RootCategoryUrl };
            var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            hpl.Load(_category.SourceUrl);
            CatalogBuild(hpl.HtmlDoc.DocumentNode, _category);
            foreach (var bottom in _category.CategoriesBottomGet())
            {
                CatalogBuildBottoms(bottom);
            }
            //foreach (var c in _category.GetCategoryList())
            //{
            //    c.SourceUrl = CategoryUrlWithIdCompose(c);
            //}
            p.Root = _category;
        }

        private void CatalogBuildBottoms(Category parent)
        {
            if (!_mti.CanContinue())
            {
                return;
            }

            _mti.AddLogFormatted("!!! CatalogBuildBottoms() started. Parent => {0}.", parent);
            var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            if (hpl.Load(parent.SourceUrl))
            {
                var subLinks = hpl.HtmlDoc.DocumentNode.SelectNodes(".//div[@class='plp-page-container__main']//div[@class='plp-navigation-slot-wrapper']//nav[@role='navigation'][@data-visual-navigation-links]/a");
                if (subLinks != null)
                {
                    foreach (var link in subLinks)
                    {
                        string name = link.InnerText;
                        string href = link.GetAttributeValue("href", "");
                        string data_tracking_label = link.GetAttributeValue("data-tracking-label", "");
                        if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(href) || AlreadyStoredForParent(parent, href))
                        {
                            continue;
                        }
                        var sub = new Category() { Name = HttpUtility.HtmlDecode(name.Trim()), SourceUrl = href };
                        string id = CategoryIdParse(data_tracking_label);
                        if (!string.IsNullOrEmpty(id))
                        {
                            sub.ID = id;
                        }
                        parent.AddCategory(sub);
                        CatalogBuildBottoms(sub);
                    }
                }
            }
        }

        private bool AlreadyStoredForParent(Category parent, string possibleNewHref)
        {
            return parent.GetParents(true).Any(c => c.SourceUrl == possibleNewHref) || parent.GetCategoryList(true).Any(c => c.SourceUrl == possibleNewHref);
        }

        private void CatalogBuild(HtmlNode node, Category parent)
        {
            if (node == null)
            {
                return;
            }
            var subNodes = node.SelectNodes("./li");
            if (subNodes != null)
            {
                foreach (var subNode in subNodes)
                {
                    var link = subNode.SelectSingleNode("./a");
                    if (link != null)
                    {
                        var href = link.GetAttributeValue("href", "");
                        var data_tracking_label = link.GetAttributeValue("data-tracking-label", "");

                        if (string.IsNullOrEmpty(href)
                            || !string.IsNullOrEmpty(data_tracking_label) && data_tracking_label.Equals("all", StringComparison.InvariantCultureIgnoreCase)
                              || link.InnerText.IndexOf(_ikeaSettings.RegionSettings.ShopAllDetectorString, StringComparison.InvariantCultureIgnoreCase) >= 0)
                        {
                            continue;
                        }

                        var ul = subNode.SelectSingleNode("./nav/ul");
                        var sub = new Category() { Name = HttpUtility.HtmlDecode(link.InnerText.Trim()), SourceUrl = href };
                        if (!string.IsNullOrEmpty(data_tracking_label))
                        {
                            sub.ID = data_tracking_label;
                        }
                        else
                        {
                            if (ul != null)
                            {
                                var ul_data_tracking_label = ul.GetAttributeValue("data-tracking-label", "");
                                string catId = CategoryIdParse(ul_data_tracking_label);
                                if (!string.IsNullOrEmpty(catId))
                                {
                                    sub.ID = catId;
                                }
                            }
                        }
                        parent.AddCategory(sub);
                        CatalogBuild(ul, sub);
                    }
                }
            }
        }

        private string CategoryUrlWithIdCompose(Category cat, string id = null)
        {
            if (string.IsNullOrEmpty(id))
            {
                id = cat.ID;
            }
            return string.Format("{0}@@{1}", cat.SourceUrl, id);
        }

        private string CategoryIdParse(string data_tracking_label)
        {
            if (!string.IsNullOrEmpty(data_tracking_label))
            {
                var arr = data_tracking_label.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
                if (arr.Length > 0)
                {
                    return arr[arr.Length - 1].Trim();
                }
            }
            return "";
        }

        private GrabProcessState _gps;
        private CacheCustom _cacheCustom = null;
        private IkeaSettings _ikeaSettings;
        public override void Login(LoginScriptParameters p)
        {
            _gps = p.State as GrabProcessState;
            _mti = p.Process.m_ti;
            p.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36";

            _ikeaSettings = IkeaSettingsBuild(_gps.GrabberSettings);
            _ikeaSettings.RegionSettings = RegionSettingsBuild(_ikeaSettings.Region);

            _gps.GrabberSettings.Settings.CacheEnabled = true; // Make enabled by default.
            _cacheCustom = new CacheCustom(_gps.CacheGet().m_cacheStorage);
        }

        private IkeaSettings IkeaSettingsBuild(GrabberSettings gs)
        {
            var ikeaSettings = new IkeaSettings();

            string minQty = gs.UserParameterGet("ProductQtyMin");
            if (!string.IsNullOrEmpty(minQty))
            {
                int mq = 0;
                if (int.TryParse(minQty, out mq))
                {
                    ikeaSettings.ProductQtyMin = mq;
                }
            }

            string minPrice = gs.UserParameterGet("ProductPriceMin");
            if (!string.IsNullOrEmpty(minPrice))
            {
                double mp = 0;
                if (double.TryParse(minPrice, out mp))
                {
                    ikeaSettings.ProductPriceMin = mp;
                }
            }

            string regionName = gs.UserParameterGet("Region");
            if (string.IsNullOrEmpty(regionName))
            {
                regionName = "EN";
            }

            Region r;
            if (Enum.TryParse(regionName.ToUpper(), out r))
            {
                ikeaSettings.Region = r;
            }

            ikeaSettings.CatalogModeEnabled = true;
            string catalogModeEnabled = gs.UserParameterGet("CatalogModeEnabled");
            if (!string.IsNullOrEmpty(catalogModeEnabled))
            {
                bool b;
                if (bool.TryParse(catalogModeEnabled, out b))
                {
                    ikeaSettings.CatalogModeEnabled = b;
                }
            }

            string pckgsWeightTotalAsAttr = gs.UserParameterGet("PackagesWeightTotalAsAttribute");
            if (!string.IsNullOrEmpty(pckgsWeightTotalAsAttr))
            {
                bool b;
                if (bool.TryParse(pckgsWeightTotalAsAttr, out b))
                {
                    ikeaSettings.PackagesWeightTotalAsAttribute = b;
                }
            }

            string prodSizesAsPckgsSizesTotal = gs.UserParameterGet("ProductSizesAsPackagesSizesTotal");
            if (!string.IsNullOrEmpty(prodSizesAsPckgsSizesTotal))
            {
                bool b;
                if (bool.TryParse(prodSizesAsPckgsSizesTotal, out b))
                {
                    ikeaSettings.ProductSizesAsPackagesSizesTotal = b;
                }
            }

            string storeIds = gs.UserParameterGet("StoreId");
            if (!string.IsNullOrEmpty(storeIds))
            {
                ikeaSettings.StoreIDs = storeIds.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList();
            }

            return ikeaSettings;
        }

        private RegionSettings RegionSettingsBuild(Region r)
        {
            var rs = new RegionSettings();
            switch (r)
            {
                case Region.EN:
                    rs.Domain = "https://www.ikea.com/us/en/";
                    rs.RootCategoryUrl = "https://www.ikea.com/us/en/header-footer/menu-products.html";
                    rs.ShopAllDetectorString = "Shop all";
                    rs.ProductAvailabilityUrlTemplate = "https://api.ingka.ikea.com/cia/availabilities/ru/us?itemNos={0}&expand=StoresList,Restocks";
                    rs.MoreProductsUrl = "https://sik.search.blue.cdtapps.com/us/en/product-list-page/more-products?category=&sort=RELEVANCE&start=&end=&c=lf&v=20200617";
                    break;
                case Region.PL:
                    rs.Domain = "https://www.ikea.com/pl/pl/";
                    rs.RootCategoryUrl = "https://www.ikea.com/pl/pl/header-footer/menu-products.html";
                    rs.ShopAllDetectorString = "Zobacz wszystko";
                    rs.ProductAvailabilityUrlTemplate = "https://api.ingka.ikea.com/cia/availabilities/ru/pl?itemNos={0}&expand=StoresList,Restocks";
                    rs.MoreProductsUrl = "https://sik.search.blue.cdtapps.com/pl/pl/product-list-page/more-products?category=&sort=RELEVANCE&start=&end=&c=lf&v=20200617";
                    break;
                case Region.RU:
                    rs.Domain = "https://www.ikea.com/ru/ru/";
                    rs.RootCategoryUrl = "https://www.ikea.com/ru/ru/header-footer/menu-products.html";
                    rs.ShopAllDetectorString = "Смотреть все";
                    rs.ProductAvailabilityUrlTemplate = "https://api.ingka.ikea.com/cia/availabilities/ru/ru?itemNos={0}&expand=StoresList,Restocks";
                    rs.MoreProductsUrl = "https://sik.search.blue.cdtapps.com/ru/ru/product-list-page/more-products?category=&sort=RELEVANCE&start=&end=&c=lf&v=20200617";
                    break;
                default:
                    break;
            }
            return rs;
        }

        private Dictionary<string, Dictionary<string, string>> _mainProductUrl2VariantsIds2Links = new Dictionary<string, Dictionary<string, string>>();

        public override void GetProductLinksForCategory(CatalogLoaderCommon.GetProductLinksForCategoryScriptParameters p)
        {
            var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            if (!hpl.Load(p.Category.SourceUrl))
            {
                return;
            }

            _mti.AddLogInfo($"_ikeaSettings.CatalogModeEnabled ? {_ikeaSettings.CatalogModeEnabled}");

            if (_ikeaSettings.CatalogModeEnabled)
            {
                Paginator paginator = new Paginator(_ikeaSettings.RegionSettings);
                string msg;
                if (!paginator.Init(hpl.HtmlDoc, out msg))
                {
                    _mti.AddLogError(string.Format("Paginator init failed. Message:'{0}'.", msg));
                    return;
                }
                int newLinksCnt = 0;
                while (paginator.MoveNext())
                {
                    string nextPage = paginator.MoreProductsLinkBuild();
                    List<string> links = new List<string>();
                    if (hpl.Load(nextPage))
                    {
                        try
                        {
                            var resp = JsonConvert.DeserializeObject<ModelsIkea.Pagination.MoreproductsResponse>(hpl.Content);
                            //links = resp.moreProducts.productWindow.Select(pw => pw.pipUrl);
                            foreach (var pw in resp.moreProducts.productWindow)
                            {
                                links.Add(pw.pipUrl);

                                /*
                                 * https://www.ikea.com/ru/ru/cat/vazy-dlya-cvetov-10776/
                                 * http://storage.mydataprovider.com/screenshots/akl/2021-02-11_14-47-22_FLmTFPxU.png
                                 * 
                                 * Не у всех товаров на их странице есть варианты, 
                                 * хотя на странице категории варианты у товара могут быть.
                                 * В MoreproductsResponse теперь приходит информация о вариантах.
                                 * Поэтому сохраняем варианты для ссылок на основной товар, чтобы взять
                                 * их при отсутствии вариантов на странице товара.
                                 * 
                                */
                                if (pw.gprDescription != null && pw.gprDescription.variants != null)
                                {
                                    _mainProductUrl2VariantsIds2Links[pw.pipUrl] = pw.gprDescription.variants.ToDictionary(v => v.id, v => v.pipUrl);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            _mti.AddLogError(ex.ToString());
                        }
                    }
                    else
                    {
                        break;
                    }

                    if (links == null || !links.Any())
                    {
                        _mti.AddLogInfo("Can't find new links.");
                        break;
                    }
                    foreach (var link in links)
                    {
                        p.Category.ProductLinks.Add(link);
                        newLinksCnt++;
                        if (newLinksCnt == hpl.Settings.MaxProductsInCategory)
                        {
                            goto OUT_OF_PAGIN;
                        }
                    }
                }
            OUT_OF_PAGIN:
                _mti.AddLogFormatted("GetProductLinksForCategory finished. Links count:{0}.", newLinksCnt.ToString());
            }
            else
            {
                _gps.CacheGet().OnPageLoadedWithError += Cache_OnPageLoadedWithError;

                //  В этих настройках парсим ссылки на все товары из категорий.
                //      https://www.ikea.com/ru/ru/header-footer/menu-products.html
                //      http://storage.mydataprovider.com/screenshots/akl/2020-10-23_09-11-43_XHHft6PA.png
                //  Если товаров в ней нет, идем в подкатегории.
                //  ==================================================================================
                //  
                //  akl_03122020 
                //  !!! UPDATE:
                //  
                //  Не во всех категориях "Смотреть все товары" возвращаются реально все товары.
                //  Напр., "Товары для ремонта" https://www.ikea.com/ru/ru/cat/tovary-dlya-remonta-hi001/
                //  не содержит все товары 
                //  из "Шурупы и крепления" https://www.ikea.com/ru/ru/cat/shurupy-i-krepleniya-49768/
                //  Поэтому, начинаем обход с подкатегорий.

                try
                {
                    TestCategory testCategory = null;
                    /*
                    testCategory = new TestCategory()
                    {
                        Root = "Кухни и бытовая техника",
                        Sub = "Кухни",
                        BottomId = "ka003"
                    };
                    */
                    if (!string.IsNullOrEmpty(_gps.GrabberSettings.UserParameterGet("TEST_CATEGORY")))
                    {
                        try
                        {
                            testCategory = JsonConvert.DeserializeObject<TestCategory>(_gps.GrabberSettings.UserParameterGet("TEST_CATEGORY"));
                        }
                        catch { }
                    }
                    AllProductsProcess(p, testCategory: testCategory);
                }
                catch (Exception ex)
                {
                    _mti.AddLogError(ex);
                }
                finally
                {
                    _gps.CacheGet().OnPageLoadedWithError -= Cache_OnPageLoadedWithError;
                    //_productsPrcsdLinks.Clear();
                }
                _mti.AddLogFormatted("GetProductLinksForCategory finished. Links count:{0}.", _productsPrcsdLinks.Count.ToString());
            }
        }

        public class TestCategory
        {
            private string _rootXpath;
            private string _subXpath;
            public string Root { get; set; }
            public string Sub { get; set; }
            internal string RootXpath
            {
                get
                {
                    if (string.IsNullOrEmpty(_rootXpath))
                    {
                        _rootXpath = string.Format("./li/a[contains(.,'{0}')]", Root);
                    }
                    return _rootXpath;
                }
            }
            internal string SubXpath
            {
                get
                {
                    if (string.IsNullOrEmpty(_subXpath))
                    {
                        _subXpath = string.Format("./nav/ul/li/a[contains(.,'{0}')]", Sub);
                    }
                    return _subXpath;
                }
            }
            public string BottomId { get; set; }
        }

        private List<string> _productsPrcsdLinks = new List<string>(100000);
        private Dictionary<string, string> _categoryName2Url = new Dictionary<string, string>();
        private Dictionary<string, string> _productUrl2FullCategoryName = new Dictionary<string, string>();
        private void AllProductsProcess(GetProductLinksForCategoryScriptParameters p, List<string> allProductsLinks = null, TestCategory testCategory = null)
        {
            int maxProductsInCategory = 100000;
            var allProductsLink2AllProductsLinkNode = new Dictionary<string, HtmlNode>();
            var allProductsLink2CategoryName = new Dictionary<string, string>();

            var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            if (allProductsLinks == null)
            {
                allProductsLinks = new List<string>();

                hpl.Load(p.Category.SourceUrl);
                string categoriesLinksXPath = testCategory != null ? testCategory.RootXpath : "./li/a";
                var categoriesLinks = hpl.HtmlDoc.DocumentNode.SelectNodes(categoriesLinksXPath);
                if (categoriesLinks == null)
                {
                    _mti.AddLogWarning("!!! Can't parse categories links.");
                    return;
                }

                foreach (var catLink in categoriesLinks)
                {
                    if (!_mti.CanContinue() || string.IsNullOrEmpty(catLink.InnerText))
                    {
                        continue;
                    }

                    var categoryName = catLink.InnerText.Trim();
                    _categoryName2Url[categoryName] = TextUtils.UrlNewCompose(_ikeaSettings.RegionSettings.Domain, catLink.Attributes["href"].Value);

                    string subCategoriesLinksXPath = testCategory != null ? testCategory.SubXpath : $"./nav/ul/li/a[not(contains(.,'{_ikeaSettings.RegionSettings.ShopAllDetectorString}'))]";
                    var subCategoriesLinksNodes = catLink.ParentNode.SelectNodes(subCategoriesLinksXPath);
                    if (subCategoriesLinksNodes == null)
                    {
                        _mti.AddLogWarning(string.Format("!!! Category:'{0}' => can't find subcategories link.", categoryName));
                        continue;
                    }

                    _mti.AddLogWarning(string.Format("!!! Category:'{0}' => subcategories links count:{1}.", categoryName, subCategoriesLinksNodes.Count.ToString()));

                    foreach (var subCategoryNode in subCategoriesLinksNodes)
                    {
                        if (!_mti.CanContinue())
                        {
                            continue;
                        }
                        string subCategoryLink = subCategoryNode.GetAttributeValue("href", "");
                        if (!string.IsNullOrEmpty(subCategoryLink))
                        {
                            string subCategoryName = subCategoryNode.InnerText.Trim();
                            subCategoryLink = TextUtils.UrlNewCompose(_ikeaSettings.RegionSettings.Domain, subCategoryLink);
                            _categoryName2Url[subCategoryName] = subCategoryLink;
                            string categoryId = CategoryIdParseFromUrl(subCategoryLink);

                            if (testCategory != null)
                            {
                                categoryId = testCategory.BottomId;
                            }

                            _mti.AddLogInfo(string.Format("!!! Subcategory link:'{0}' => category ID:'{1}'.", subCategoryLink, categoryId));
                            if (string.IsNullOrEmpty(categoryId))
                            {
                                continue;
                            }

                            /*
                             * akl_03022021:
                             * Т.к. утоваров без крошек в карточке отсутствует ссылка на их непосредственную категорию,
                             * придется сделать минимум еще один запрос, чтобы собрать имена-ссылки подкатегорий здесь.
                             */
                            if (hpl.Load(subCategoryLink))
                            {
                                foreach (var name2url in CategoriesName2UrlParse(hpl.Content))
                                {
                                    _categoryName2Url[name2url.Key] = name2url.Value;
                                }
                            }

                            var links = new List<string>();
                            Models.Pagination.MoreproductsResponse resp = null;

                            if (hpl.Load(string.Format("https://sik.search.blue.cdtapps.com/ru/ru/product-list-page/more-products?category={0}&sort=RELEVANCE&start=0&end={1}&c=lf&v=20210126",
                    categoryId, maxProductsInCategory.ToString())))
                            {
                                try
                                {
                                    resp = JsonConvert.DeserializeObject<Models.Pagination.MoreproductsResponse>(hpl.Content);
                                    //links = resp.moreProducts.productWindow.Select(pw => pw.pipUrl);
                                    foreach (var pw in resp.moreProducts.productWindow)
                                    {
                                        links.Add(pw.pipUrl);

                                        /*
                                         * https://www.ikea.com/ru/ru/cat/vazy-dlya-cvetov-10776/
                                         * http://storage.mydataprovider.com/screenshots/akl/2021-02-11_14-47-22_FLmTFPxU.png
                                         * 
                                         * Не у всех товаров на их странице есть варианты, 
                                         * хотя на странице категории варианты у товара могут быть.
                                         * В MoreproductsResponse теперь приходит информация о вариантах.
                                         * Поэтому сохраняем варианты для ссылок на основной товар, чтобы взять
                                         * их при отсутствии вариантов на странице товара.
                                         * 
                                        */
                                        if (pw.gprDescription != null && pw.gprDescription.variants != null)
                                        {
                                            _mainProductUrl2VariantsIds2Links[pw.pipUrl] = pw.gprDescription.variants.ToDictionary(v => v.id, v => v.pipUrl);
                                        }

                                        /*
                                         * akl_02262021:
                                         * Есть товары, в карточке которых отсутствуют крошки. По этой причине,
                                         * нужно мапить ссылку товара на полную категорию на этапе сбора ссылок.
                                         */
                                        _productUrl2FullCategoryName[pw.pipUrl] = string.Format("{0}->{1}", categoryName, subCategoryName);
                                    }
                                }
                                catch (Exception ex)
                                {
                                    _mti.AddLogError(ex.ToString());
                                }
                            }
                            else
                            {
                                continue;
                            }

                            _mti.AddLogFormatted("!!! Category Link '{0}' => products links num by api: {1}.", subCategoryLink, links != null ? links.Count().ToString() : "0");

                            //if (links == null)
                            //{
                            //    continue;
                            //}
                            foreach (var link in links)
                            {
                                if (_productsPrcsdLinks.Contains(link))
                                {
                                    _mti.AddLogFormatted("!!! Product link '{0}' stored allready.", link);
                                    continue;
                                }

                                _productsPrcsdLinks.Add(link);

                                p.Category.ProductLinks.Add(link);
                                if (_productsPrcsdLinks.Count == hpl.Settings.MaxProductsInCategory)
                                {
                                    goto OUT_OFF_LOOPS;
                                }
                            }
                        }
                    }
                }
            }

        OUT_OFF_LOOPS:
            _mti.AddLogInfo("[AllProductsProcess] Finished.");
        }

        private string CategoryIdParseFromUrl(string link)
        {
            var urlNormal = TextUtils.UrlNewCompose(_ikeaSettings.RegionSettings.Domain, link);
            Uri u;
            if (Uri.TryCreate(urlNormal, UriKind.RelativeOrAbsolute, out u))
            {
                return Regex.Replace(u.AbsolutePath, @"^[\w\W]+-", "").TrimEnd('/');
            }
            return "";
        }

        private List<string> _bottomsCategoriesXpathList = new List<string>()
        {
            ".//div[contains(@class,'plp-category-link-list')]//li[contains(@class,'plp-category-link-list__item')]/a",
            ".//nav[@role='navigation']/a"
        };
        private Dictionary<string, string> CategoriesName2UrlParse(string html)
        {
            var hDoc = new HtmlDocument();
            hDoc.LoadHtml(html);
            foreach (var xpath in _bottomsCategoriesXpathList)
            {
                var nodes = hDoc.DocumentNode.SelectNodes(xpath);
                if (nodes != null)
                {
                    var n2u = new Dictionary<string, string>();
                    foreach (var n in nodes)
                    {
                        string h = n.GetAttributeValue("href", "");
                        if (string.IsNullOrEmpty(n.InnerText) || string.IsNullOrEmpty(h))
                        {
                            continue;
                        }
                        n2u[n.InnerText.Trim()] = TextUtils.UrlNewCompose(_ikeaSettings.RegionSettings.Domain, h);
                    }
                    _mti.AddLogInfo("[CategoriesName2UrlParse] Result ==================");
                    _mti.AddLogInfo(string.Join("\r\n", n2u.Select(kvp => string.Format("{0} => {1}", kvp.Key, kvp.Value))));
                    _mti.AddLogInfo("====================================================");

                    return n2u;
                }
            }
            return new Dictionary<string, string>();
        }

        private bool Cache_OnPageLoadedWithError(Cache cacheProvider, HttpWebResponse response, out string content)
        {
            content = "";

            if (response.ContentLength != 0)
            {
                _mti.AddLogWarning("In CustomScript_OnPageLoadedWithError");
                try
                {
                    var dataContent = response.GetResponseStream().ReadAsBytes();
                    var exceptionContent = cacheProvider.Encoding.GetString(dataContent);
                    if (exceptionContent.Contains("Unknown category key"))
                    {
                        content = exceptionContent;
                        return true;
                    }
                }
                catch (Exception)
                {
                }
            }

            return !string.IsNullOrEmpty(content);
        }

        internal class Paginator
        {
            /*
             * https://sik.search.blue.cdtapps.com/ru/ru/product-list-page/more-products?sessionId=5980bee3-2658-4788-bb3c-501cd50287f3&category=11703&sort=RELEVANCE&start=24&end=48&c=lf&v=20200617
             * Значение параметра запроса "sessionId" совпадает с local storage ключем "search-sik-session-ruru".
             * Источник возвращает валидные данные и без "sessionId", поэтому, пока что, игнорим этот параметр.
             */
            private int _currentIdx;
            private int _lastIdx = 0;
            private int _shift;
            private ModelsIkea.Pagination.DataCategory _dataCategory;
            private RegionSettings _regionSettings;
            public Paginator(RegionSettings rs, int startIdx = 0, int shift = 24)
            {
                _regionSettings = rs;
                _currentIdx = startIdx - 1;
                _shift = shift;
            }
            internal bool Init(HtmlDocument htmlDoc, out string msg)
            {
                msg = string.Empty;
                bool r = false;
                string dataCategoryStr = TextUtils.GetHtmlValue(htmlDoc, ".//div[contains(@class,'js-product-list')]", "data-category", true);
                if (!string.IsNullOrEmpty(dataCategoryStr))
                {
                    try
                    {
                        _dataCategory = JsonConvert.DeserializeObject<ModelsIkea.Pagination.DataCategory>(dataCategoryStr);
                        _lastIdx = _dataCategory.totalCount / _shift;
                        if (_dataCategory.totalCount % _shift > 0)
                        {
                            _lastIdx++;
                        }
                        r = true;
                    }
                    catch (Exception ex)
                    {
                        msg = ex.ToString();
                    }
                }
                else
                {
                    msg = "dataCategoryStr is EMPTY";
                }
                return r;
            }
            internal string MoreProductsLinkBuild()
            {
                var uBuilder = new UriBuilder(_regionSettings.MoreProductsUrl);
                var query = HttpUtility.ParseQueryString(uBuilder.Query);
                query["category"] = _dataCategory.id;
                query["start"] = (_currentIdx * _shift).ToString();
                query["end"] = (_currentIdx * _shift + _shift).ToString();
                uBuilder.Query = query.ToString();
                return uBuilder.ToString();
            }

            internal bool MoveNext()
            {
                _currentIdx++;
                return _currentIdx <= _lastIdx;
            }
        }

        public override void RunCategory(RunCategoryScriptParameters p) { }

        private void Init(RunProductScriptParameters p)
        {
            _mti = p.Process.m_ti;
            _product = p.Product;
            _category = p.Category;
        }

        public override void RunProduct(RunProductScriptParameters p)
        {
            try
            {
                Init(p);

                _category = p.Category;
                _product.Name = HttpUtility.HtmlDecode(_product.Name);
                _product.Name = _product.GetAttributeValue("SYS_PRODUCT_NAME2") + " " + _product.Name;

                if (_cacheCustom.IsProcessed(_product.Art))
                {
                    _mti.AddLogFormatted("!!! Product ART:'{0}' will be skipped because processed early.", _product.Art);
                    return;
                }

                if (_product.PriceIsOk)
                    _product.Price = _product.Price/*.Replace(".", ",")*/.Replace(" ", string.Empty);
                else
                    _product.Price = "0";

                bool mainVariantProcessed = false;

                bool variantsHtmlFromProductPage = false;
                if (!string.IsNullOrEmpty(_product.GetAttributeValue("VARIATION_SECTION")))
                {
                    variantsHtmlFromProductPage = true;
                }
                else if (!string.IsNullOrEmpty(_product.GetAttributeValue("PRODUCT_STYLES")))
                {
                    //https://www.ikea.com/ru/ru/p/kolbyorn-shkaf-d-doma-sada-zelenyy-40455908/
                    //для товаров с комбинациями только по одной опции.
                    variantsHtmlFromProductPage = true;
                }

                Dictionary<string, string> variantsIDs2Links = new Dictionary<string, string>();
                if (variantsHtmlFromProductPage)
                {
                    //VariantsIDs2LinksPopulate(product.GetAttributeValue("VARIATION_SECTION"), product.GetAttributeValue("PRODUCT_STYLES"),
                    //    variantsIDs2Links, new List<string>() { product.Url });
                    variantsIDs2Links = VariantsIDs2LinksGet(_product.GetAttributeValue("VARIATION_SECTION"), _product.GetAttributeValue("PRODUCT_STYLES"));
                }
                else
                {
                    var variantsIDs2LinksStoredFromCategoryPage = new Dictionary<string, string>();
                    if (_mainProductUrl2VariantsIds2Links.TryGetValue(_product.Url, out variantsIDs2LinksStoredFromCategoryPage))
                    {
                        _mti.AddLogFormatted("!!! Variants from category page will be used => product url:'{0}'", _product.Url);
                        variantsIDs2Links = variantsIDs2LinksStoredFromCategoryPage;
                        //Нужно принудительно обработать вариант основного товара, если берем варианты из категории.
                        variantsIDs2Links[_product.Art] = _product.Url;
                    }
                }

                var nonProcessedVariantsIds2Links = new Dictionary<string, string>();
                foreach (var id2v in variantsIDs2Links)
                {
                    if (_cacheCustom.IsProcessed(id2v.Key))
                    {
                        _mti.AddLogFormatted("!!! Variant ART:'{0}' will be skipped because processed early.", id2v.Key);
                    }
                    else
                    {
                        nonProcessedVariantsIds2Links[id2v.Key] = id2v.Value;
                    }
                }

                if (nonProcessedVariantsIds2Links.Count > 0)
                {
                    var vIds2LinksForBulkLoading = new Dictionary<string, string>();
                    foreach (var id2Link in nonProcessedVariantsIds2Links)
                    {
                        string html;
                        if (!_cacheCustom.Load(id2Link.Value, out html))
                        {
                            vIds2LinksForBulkLoading[id2Link.Key] = id2Link.Value;
                        }
                    }
                    if (vIds2LinksForBulkLoading.Count > 0)
                    {
                        VariantsBulkLoad(vIds2LinksForBulkLoading);
                    }

                    var cmbs = new List<Combination>();
                    foreach (var varId2Link in nonProcessedVariantsIds2Links)
                    {
                        Product productForProcessing;
                        if (varId2Link.Key.Equals(_product.Art, StringComparison.InvariantCultureIgnoreCase))
                        {
                            productForProcessing = _product;
                            mainVariantProcessed = true;
                        }
                        else
                        {
                            productForProcessing = new Combination();
                            productForProcessing.Art = productForProcessing.ID = varId2Link.Key.ToUpper();
                            productForProcessing.Url = varId2Link.Value;
                        }
                        if (VariantProcessing(productForProcessing))
                        {
                            var cmb = productForProcessing as Combination;
                            if (cmb != null)
                            {
                                cmbs.Add(cmb);
                            }
                        }
                    }
                    _product.CombinationsAdd(cmbs);
                    _product.SetAttributeValue("VARIATION_SECTION", "");
                    /*
                         * Нужно обрабытывать основной товар даже при наличии у него вариантов, т.к. некоторые товары не попадают в свои варианты.
                         * Напр. https://www.ikea.com/ru/ru/p/metod-navesnoy-shkaf-so-steklyannoy-dveryu-belyy-budbin-seryy-s89227745/
                        */
                    if (!mainVariantProcessed)
                    {
                        VariantProcessing(_product);
                        mainVariantProcessed = true;
                    }

                    var productsForSave = new List<Product>() { _product };
                    foreach (var prod in ProductsFromCombinationsGet(_product))
                    {
                        if (productsForSave.Any(pr => pr.Art == prod.Art))
                        {
                            continue;
                        }
                        productsForSave.Add(prod);
                    }
                    _product.CombinationsRemove();

                    var grabHelper = _gps.ProcessMain as GrabHelper;
                    foreach (var pr in productsForSave)
                    {
                        if (_cacheCustom.IsProcessed(pr.Art))
                        {
                            _mti.AddLogFormatted("!!! Product ART:'{0}' will be skipped because processed early.", pr.Art);
                            continue;
                        }
                        _cacheCustom.SaveAsProcessed(pr.Art);

                        if ((_ikeaSettings.ProductQtyMin > 0 && pr.QuantityInt() < _ikeaSettings.ProductQtyMin)
                            || (_ikeaSettings.ProductPriceMin > 0 && pr.PriceDoubleUnsafe < _ikeaSettings.ProductPriceMin))
                        {
                            continue;
                        }

                        var tempProd = HtmlBlocksClear(pr);
                        tempProd.Category = p.Category;
                        ProductImagesRename(tempProd);
                        ProductImagesNumberTruncate(tempProd, 40);
                        tempProd.Art = tempProd.ID = IDCreate(tempProd.Art);
                        string strError = null;
                        if (grabHelper == null || !grabHelper._grabProcessParameters.EmbProvider.ProductSave(tempProd, out strError))
                        {
                            _mti.AddLogError(string.Format("Database provider: {0}", strError));
                        }
                    }
                }
                ////if (_productQtyMin > 0 && product.QuantityInt() < _productQtyMin)
                //{
                //product.SavedToStorage = product.SaveIt = false;
                //}
            }
            finally
            {
                _product.SavedToStorage = _product.SaveIt = false;
                _cacheCustom.HtmlCacheClear();
            }
        }

        private void VariantsBulkLoad(Dictionary<string, string> variantsIDs2Links)
        {
            _mti.AddLogFormatted("[VariantsBulkLoad] Started. variantsIDs2Links:{0}.", variantsIDs2Links.Count);
            var bulkRequests = new List<CatalogLoader.Utils.HtmlBulkRequests.HttpWebRequestEntry>();
            foreach (var id2Link in variantsIDs2Links)
            {
                var r = HttpWebRequest.Create(id2Link.Value) as HttpWebRequest;
                r.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36";
                r.CookieContainer = _gps.m_cookies_con;
                bulkRequests.Add(new CatalogLoader.Utils.HtmlBulkRequests.HttpWebRequestEntry()
                {
                    Key = id2Link.Key,
                    Request = r
                });
            }

            var hbr = HtmlBulkRequestsGet();
            _mti.AddLogFormatted("[VariantsBulkLoad] !!! hbr.Process() started => hbr._requests.Count:{0}.", bulkRequests.Count.ToString());
            hbr.Process(bulkRequests);
            _mti.AddLogFormatted("[VariantsBulkLoad] !!! hbr.Process() finished => hbr._unprocessedRequests.Count:{0}.", hbr._unprocessedRequests.Count.ToString());

            foreach (var id2Link in variantsIDs2Links)
            {
                var requestEntry = _hbr._responses.FirstOrDefault(t => t.Key == id2Link.Key);
                if (requestEntry.Failed)
                {
                    throw new Exception(string.Format("[VariantsBulkLoad] request entry with key:'{0}' failed.", requestEntry.Key));
                }
                _cacheCustom.SaveHtml(id2Link.Value, requestEntry.Content);
            }
            _mti.AddLogInfo("[VariantsBulkLoad] Finished.");
        }

        internal sealed class QtyItem
        {
            public int Value { get; set; }
            public string StoreId { get; set; }
            public override string ToString()
            {
                return $"Value:{Value}, StoreId:{StoreId}";
            }
        }

        internal sealed class CacheCustom
        {
            private CacheStorage _cacheStorage;
            private Dictionary<string, string> _url2Html;
            private Dictionary<string, List<QtyItem>> _art2Qty;
            private HashSet<string> _productsArtProcessed = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
            public CacheCustom(CacheStorage cacheStorage)
            {
                _cacheStorage = cacheStorage;
                _url2Html = new Dictionary<string, string>();
                _art2Qty = new Dictionary<string, List<QtyItem>>();
            }
            internal bool IsCached(string url)
            {
                string html;
                if (!_url2Html.TryGetValue(url, out html))
                {
                    if (_cacheStorage.Load(url, out html))
                    {
                        _url2Html[url] = html;
                    }
                }
                return !string.IsNullOrEmpty(html);
            }
            internal bool Load(string url, out string html)
            {
                html = "";
                if (IsCached(url))
                {
                    html = _url2Html[url];
                    return true;
                }
                return false;
            }
            internal void SaveHtml(string url, string html)
            {
                if (!string.IsNullOrEmpty(url))
                {
                    _url2Html[url] = html;
                    _cacheStorage.Save(url, html);
                }
            }
            internal void SaveQty(string url, QtyItem qty)
            {
                if (!string.IsNullOrEmpty(url))
                {
                    if (!_art2Qty.TryGetValue(url, out List<QtyItem> qts))
                    {
                        qts = new List<QtyItem>();
                        _art2Qty[url] = qts;
                    }
                    qts.Add(qty);
                }
            }
            internal void HtmlCacheClear()
            {
                _url2Html.Clear();
            }
            internal bool GetQty(string art, out List<QtyItem> qtyLIst)
            {
                return _art2Qty.TryGetValue(art, out qtyLIst);
            }
            internal bool IsProcessed(string art)
            {
                if (!string.IsNullOrEmpty(art))
                {
                    return _productsArtProcessed.Contains(art);
                }
                return false;
            }

            internal void SaveAsProcessed(string art)
            {
                if (!string.IsNullOrEmpty(art))
                {
                    _productsArtProcessed.Add(art);
                }
            }
        }

        //private Dictionary<string, int> _variantId2Qty = new Dictionary<string, int>();
        CatalogLoader.Utils.HtmlBulkRequests _hbr = null;
        private Dictionary<string, string> VariantsIDs2LinksGet(string variationSection, string productStyles)
        {
            /*
             * akl_12102020
             * 
             * http://crm.catalogloader.com/issues/35427#note-88
             * https://www.ikea.com/ru/ru/p/longfell-rabochiy-stul-s-podlokotnikami-gunnared-bezhevyy-chernyy-s29210026/
             * "В выгрузку попадают только товары с разным основанием: белый, чёрный.
                Разные цвета чехлов не попадают - только бежевый.
                Таким образом из 8 вариантов в xml попали 2."
             * =========================================================================================================
             * 
             * Нужно перебирать все варианты ("VARIATION_SECTION"), переходить в карточку каждого
             * и там уже собирать стили ("PRODUCT_STYLES").
             * Иначе, теряются комбинации стилей.
             * ==========================================================================================================
             * 
             * akl_18112020 
             * 
             * http://crm.catalogloader.com/issues/35427#note-111
             * "Предложений должно быть 32, а выгружаеся только 8"
             * https://www.ikea.com/ru/ru/p/malm-karkas-krovati-dubovyy-shpon-belenyy-luroy-s39210945/
             * 
             * !!! UP: После перехода в карточку варианта, нужно собирать не только стили, но и сами варианты.
             * Иначе, список комбинаций будет неполным.
             * 
             * akl_06122021
             * Похоже, переход по ссылке каждого варианта больше не актуален - берем только то,
             * что пришло на странице основного товара и на странице каждого стиля.
            */

            _mti.AddLogInfo("[VariantsIDs2LinksGet] Started.");
            var ids2Variants = new Dictionary<string, Variant>();

            // Берем все варианты стилей, если есть...
            var stylesId2Variant = new Dictionary<string, Variant>();
            if (!string.IsNullOrEmpty(productStyles))
            {
                foreach (var id2Variant in Ids2VariantsParse(productStyles, typeof(ModelsIkea.Variations.Variation)))
                {
                    if (!_mti.CanContinue())
                    {
                        break;
                    }
                    stylesId2Variant[id2Variant.Key] = id2Variant.Value;
                }
            }
            // ...собираем варианты на странице каждого неактивного стиля...
            foreach (var styleId2Variant in stylesId2Variant)
            {
                if (!_mti.CanContinue())
                {
                    break;
                }

                if (!styleId2Variant.Value.IsSelected)
                {
                    var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
                    hpl.Load(styleId2Variant.Value.Url);
                    var variantsNode = hpl.HtmlDoc.DocumentNode.SelectSingleNode(".//div[contains(@class,'js-product-variation-section')]");
                    if (variantsNode != null)
                    {
                        string VARIATION_SECTION = HttpUtility.HtmlDecode(variantsNode.GetAttributeValue("data-initial-props", ""));
                        if (!string.IsNullOrEmpty(VARIATION_SECTION))
                        {
                            foreach (var id2Variant in Ids2VariantsParse(VARIATION_SECTION, typeof(ModelsIkea.Variations.VariationsSection)))
                            {
                                if (!_mti.CanContinue())
                                {
                                    break;
                                }
                                ids2Variants[id2Variant.Key] = id2Variant.Value;
                            }
                        }
                        else
                        {
                            //...если вариантов нет, то берем сам стиль...
                            ids2Variants[styleId2Variant.Key] = styleId2Variant.Value;
                        }
                    }
                }
            }
            // ...собираем варианты на странице активного стиля...
            if (!string.IsNullOrEmpty(variationSection))
            {
                foreach (var id2Variant in Ids2VariantsParse(variationSection, typeof(ModelsIkea.Variations.VariationsSection)))
                {
                    if (!_mti.CanContinue())
                    {
                        break;
                    }
                    ids2Variants[id2Variant.Key] = id2Variant.Value;
                }
            }
            else
            {
                var styleSelectedId2Var = stylesId2Variant.FirstOrDefault(id2v => id2v.Value.IsSelected);
                if (!string.IsNullOrEmpty(styleSelectedId2Var.Key))
                {
                    ids2Variants[styleSelectedId2Var.Key] = styleSelectedId2Var.Value;
                }
            }

            var nonProcessedVariants = new List<Variant>();
            foreach (var id2v in ids2Variants)
            {
                if (_cacheCustom.IsProcessed(id2v.Key))
                {
                    _mti.AddLogFormatted("!!! Variant ART:'{0}' will be skipped because processed early.", id2v.Key);
                }
                else if (_ikeaSettings.ProductPriceMin > 0 && id2v.Value.Price < _ikeaSettings.ProductPriceMin)
                {
                    _mti.AddLogFormatted("!!! Variant '{0}' processing interrupt -> current price ({1}) < minimum price allowable value ({2})",
                        id2v.Key, id2v.Value.Price.ToString(), _ikeaSettings.ProductPriceMin.ToString());

                    _cacheCustom.SaveAsProcessed(id2v.Key);
                }
                else
                {
                    nonProcessedVariants.Add(id2v.Value);
                }
            }

            List<Variant> varsValid = VariantsAvailabilityValidation(nonProcessedVariants);
            _mti.AddLogInfo("[VariantsIDs2LinksGet] Finished.");
            return varsValid.Count > 0 ? varsValid.ToDictionary(v => v.Id, v => v.Url) : new Dictionary<string, string>();
        }

        CatalogLoader.Utils.HtmlBulkRequests HtmlBulkRequestsGet()
        {
            if (_hbr == null)
            {
                _hbr = new CatalogLoader.Utils.HtmlBulkRequests();
                _hbr.ThreadsNumber = 20;
                _hbr.ProxySourceList = "http://ru.proxy-doctor.com/WebApi/CampaignDataLatestGet/1-201705051618-NNCIAPGJEYMVQKGXQRXKFKCOKJLDNMRVBFBQQMLXHPQHYBMDXK?collection-name=ikearu";
                _hbr.BanDetectionString = "The following error was encountered while trying to retrieve the URL[next]Method Not Allowed[next]Access Denied[next]非";
                //_hbr.RequestAttempts = _gps.GrabberSettings.Settings.RequestAttempts;
                _hbr.RequestAttempts = 10;
                _hbr.RequestTimeout = _gps.GrabberSettings.Settings.RequestTimeout;
                _hbr.CustomRequestUse = true;
            }
            return _hbr;
        }

        private List<Variant> VariantsAvailabilityValidation(List<Variant> variants)
        {
            _mti.AddLogFormatted("[VariantsAvailabilityValidation] Started. variants total:{0}.", variants.Count.ToString());
            var varsToProcess = new List<Variant>();
            var id2IdNorm = new Dictionary<string, string>();
            foreach (var v in variants)
            {
                if (!_cacheCustom.GetQty(v.Id, out List<QtyItem> qts))
                {
                    varsToProcess.Add(v);
                }
            }
            if (varsToProcess.Count > 0)
            {
                var bulkRequests = new List<CatalogLoader.Utils.HtmlBulkRequests.HttpWebRequestEntry>();
                _mti.AddLogFormatted("[VariantsAvailabilityValidation] !!!varsToProcess.Count:{0}.", varsToProcess.Count.ToString());
                var idNorm2Qhtml = new Dictionary<string, string>(varsToProcess.Count);
                foreach (var vp in varsToProcess)
                {
                    string idNorm = vp.Id.ToUpper();
                    if (idNorm.StartsWith("S"))
                    {
                        idNorm = idNorm.TrimStart('S');
                    }
                    id2IdNorm[vp.Id] = idNorm;

                    //var aviTest = ProductAvailabilityInfoGet(_gps.GrabberSettings.UserParameterGet("StoreId"), id2IdNorm[vp.Id]);

                    //string qUrl = string.Format("https://api.ingka.ikea.com/cia/availabilities/ru/us?itemNos={0}&expand=StoresList,Restocks", idNorm);
                    string qUrl = string.Format(_ikeaSettings.RegionSettings.ProductAvailabilityUrlTemplate, idNorm);
                    string qHtml;
                    if (!_cacheCustom.Load(qUrl, out qHtml))
                    {
                        var r = HttpWebRequest.Create(qUrl) as HttpWebRequest;
                        r.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36";
                        r.Accept = "application/json;version=2";
                        r.Headers["X-Client-Id"] = "b6c117e5-ae61-4ef5-b4cc-e0b1e37f0631";
                        r.Headers["accept-encoding"] = "gzip, deflate, br";
                        r.CookieContainer = _gps.m_cookies_con;
                        bulkRequests.Add(new CatalogLoader.Utils.HtmlBulkRequests.HttpWebRequestEntry()
                        {
                            Key = vp.Id,
                            Request = r
                        });
                    }
                    else
                    {
                        idNorm2Qhtml[idNorm] = qHtml;
                    }
                }

                var hbr = HtmlBulkRequestsGet();
                _mti.AddLogFormatted("[VariantsAvailabilityValidation] !!! hbr.Process() started => hbr._requests.Count:{0}.", bulkRequests.Count.ToString());
                hbr.Process(bulkRequests);
                _mti.AddLogFormatted("[VariantsAvailabilityValidation] !!! hbr.Process() finished => hbr._unprocessedRequests.Count:{0}.", hbr._unprocessedRequests.Count.ToString());

                foreach (var vp in varsToProcess)
                {
                    string qHtml = "";
                    var requestEntry = _hbr._responses.FirstOrDefault(t => t.Key == vp.Id);
                    if (requestEntry != null)
                    {
                        if (requestEntry.Failed)
                        {
                            throw new Exception(string.Format("[VariantsAvailabilityValidation] request entry with key:'{0}' failed.", requestEntry.Key));
                        }
                        else
                        {
                            qHtml = requestEntry.Content;
                            _cacheCustom.SaveHtml(requestEntry.Request.Address.AbsoluteUri, requestEntry.Content);
                        }

                    }
                    else
                    {
                        idNorm2Qhtml.TryGetValue(id2IdNorm[vp.Id], out qHtml);
                    }

                    foreach (var storeId in _ikeaSettings.StoreIDs)
                    {
                        var avi = ProductAvailabilityInfoGet(storeId, id2IdNorm[vp.Id], qHtml);
                        _cacheCustom.SaveQty(vp.Id, new QtyItem() { Value = avi.Qty, StoreId = storeId });
                    }
                }
            }

            List<Variant> validVars = null;
            if (_ikeaSettings.ProductQtyMin > 0)
            {
                validVars = new List<Variant>();
                foreach (var v in variants)
                {
                    if (_cacheCustom.GetQty(v.Id, out List<QtyItem> qtyList))
                    {
                        int qtySum = qtyList.Sum(q => q.Value);
                        if (qtySum < _ikeaSettings.ProductQtyMin)
                        {
                            _mti.AddLogWarning(string.Format("[VariantsAvailabilityValidation] Variant '{0}' processing interrupt -> current quantity ({1}) < minimum allowable value ({2})", v.Id, qtySum.ToString(), _ikeaSettings.ProductQtyMin.ToString()));
                            _cacheCustom.SaveAsProcessed(v.Id);
                            continue;
                        }
                    }
                    validVars.Add(v);
                }
            }
            else
            {
                validVars = variants;
            }
            _mti.AddLogFormatted("[VariantsAvailabilityValidation] Finished => valid variants/variants:{0}/{1}.", validVars.Count.ToString(), variants.Count.ToString());
            return validVars;
        }

        private Dictionary<string, Variant> Ids2VariantsParse(string html, Type type)
        {
            _mti.AddLogInfo("<<<<< Ids2VariantsParse started >>>>>");
            if (string.IsNullOrEmpty(html))
            {
                return new Dictionary<string, Variant>();
            }
            var ids2Vars = new Dictionary<string, Variant>();
            try
            {
                if (type == typeof(ModelsIkea.Variations.VariationsSection))
                {
                    var j = JsonConvert.DeserializeObject<ModelsIkea.Variations.VariationsSection>(html);
                    foreach (var vrnt in j.variations)
                    {
                        var vrntOptions = vrnt.allOptions.Count > vrnt.options.Count ? vrnt.allOptions : vrnt.options;
                        foreach (var opt in vrntOptions)
                        {
                            if (!string.IsNullOrEmpty(opt.linkId) && !string.IsNullOrEmpty(opt.url))
                            {

                                ids2Vars[opt.linkId] = new Variant() { Id = opt.linkId, Url = opt.url, IsSelected = opt.isSelected, Price = opt.priceProps.revampPrice.PriceDoubleGet() };
                            }
                        }
                    }
                }
                else if (type == typeof(ModelsIkea.Variations.Variation))
                {
                    var json = JObject.Parse(html);
                    var j = JsonConvert.DeserializeObject<ModelsIkea.Variations.Variation>(json["variationStyles"].First.ToString());
                    var options = j.allOptions.Count > j.options.Count ? j.allOptions : j.options;
                    foreach (var opt in options)
                    {
                        if (!string.IsNullOrEmpty(opt.linkId) && !string.IsNullOrEmpty(opt.url))
                        {
                            ids2Vars[opt.linkId] = new Variant() { Id = opt.linkId, Url = opt.url, IsSelected = opt.isSelected, Price = opt.priceProps.revampPrice.PriceDoubleGet() };
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                _mti.AddLogError(ex);
            }

            _mti.AddLogInfo("<<< variants count: " + ids2Vars.Count.ToString());
            return ids2Vars;
        }

        private Dictionary<string, string> VariantsIDs2LinksParse(string html, Type type)
        {
            _mti.AddLogInfo("<<<<< VariantsIDs2LinksParse started >>>>>");
            if (string.IsNullOrEmpty(html))
            {
                return new Dictionary<string, string>();
            }

            var ids2Links = new Dictionary<string, string>();
            try
            {
                if (type == typeof(ModelsIkea.Variations.VariationsSection))
                {
                    var j = JsonConvert.DeserializeObject<ModelsIkea.Variations.VariationsSection>(html);
                    foreach (var vrnt in j.variations)
                    {
                        var vrntOptions = vrnt.allOptions.Count > vrnt.options.Count ? vrnt.allOptions : vrnt.options;
                        foreach (var opt in vrntOptions)
                        {
                            if (!string.IsNullOrEmpty(opt.linkId) && !string.IsNullOrEmpty(opt.url))
                            {
                                ids2Links[opt.linkId] = opt.url;
                            }
                        }
                    }
                }
                else if (type == typeof(ModelsIkea.Variations.Variation))
                {
                    var j = JsonConvert.DeserializeObject<ModelsIkea.Variations.Variation>(html);
                    var options = j.allOptions.Count > j.options.Count ? j.allOptions : j.options;
                    foreach (var opt in options)
                    {
                        if (!string.IsNullOrEmpty(opt.linkId) && !string.IsNullOrEmpty(opt.url))
                        {
                            ids2Links[opt.linkId] = opt.url;
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                _mti.AddLogError(ex);
            }

            _mti.AddLogInfo("<<< variants IDs2Links count: " + ids2Links.Count.ToString());
            return ids2Links;
        }

        private HttpMethodParameters _hmpForVariantsLoad = new HttpMethodParameters()
        {
            MethodName = "GET",
            Accept = "application/vnd.ikea.iows+json;version=2.0",
            httpHeaders = new Dictionary<string, string>()
            {
                /*
                 * Headers logic info script.
                 * https://ikeasources.ru/b/m2.prod.js?v=156940268994123778112121
                 */
            //{ "Sec-Fetch-Mode", "cors" },
            //{ "Consumer", "DEVRU" },
            { "Consumer", "MAMMUT" },
                //{ "Contract", "39799" },
                { "Contract", "37249" },
                { "accept-encoding", "gzip, deflate, br" },
            }
        };
        private bool VariantProcessing(Product productForProcessing)
        {
            _mti.AddLogInfo("<<<<< VariantProcessing started >>>>>");
            _mti.AddLogInfo("<<< product for processing type:" + productForProcessing.GetType());
            _mti.AddLogInfo("<<< product for processing id:" + productForProcessing.Art);

            string artPrepared = productForProcessing.Art.ToUpper();
            if (artPrepared.StartsWith("S"))
            {
                artPrepared = artPrepared.TrimStart('S');
            }

            if (!_cacheCustom.GetQty(productForProcessing.Art, out List<QtyItem> qtyList))
            {
                qtyList = new List<QtyItem>();
                string qtyHtml;
                _cacheCustom.Load(string.Format("https://api.ingka.ikea.com/cia/availabilities/ru/us?itemNos={0}&expand=StoresList,Restocks", artPrepared), out qtyHtml);
                foreach (var storeId in _ikeaSettings.StoreIDs)
                {
                    var avi = ProductAvailabilityInfoGet(storeId, artPrepared, qtyHtml);
                    if (avi.Qty < 1)
                    {
                        avi.Qty = 0;
                    }
                    var qtyItem = new QtyItem() { Value = avi.Qty, StoreId = storeId };
                    _cacheCustom.SaveQty(productForProcessing.Art, qtyItem);
                    qtyList.Add(qtyItem);
                }
            }

            int qty = qtyList.Sum(q => q.Value);
            productForProcessing.Quantity = qty.ToString();

            foreach (var item in qtyList)
            {
                productForProcessing.AddDynamicAttribute($"QTY[{item.StoreId}]", item.Value.ToString());
            }

            _mti.AddLogInfo($"[VariantProcessing] Variant '{productForProcessing.Art}' QTY list:{string.Join("[next]", qtyList.Select(q => q.ToString()))}");

            //if (qty == 0)
            if (_ikeaSettings.ProductQtyMin > 0 && qty < _ikeaSettings.ProductQtyMin)
            {
                _mti.AddLogWarning(string.Format("[VariantProcessing] Variant '{0}' processing interrupt -> current quantity ({1}) < minimum allowable value ({2})",
                    productForProcessing.Art, qty, _ikeaSettings.ProductQtyMin));

                return true;
            }

            /*
            string productHtml = "";
            if (!_url2ProductPageHtml.TryGetValue(productForProcessing.Url, out productHtml))
            {
                var loader = _gps.Proxy.GetHtmlPageLoaderEmpty();
                loader.Load(productForProcessing.Url);
                _url2ProductPageHtml[productForProcessing.Url] = productHtml = loader.Content;
            }
            */

            //string variantJsonUrl = string.Format("https://iows.ikea.com/retail/iows/us/en/catalog/items/{0},{1}?ignoreErrors=true", pathSegment.ToUpper(), artPrepared);
            //var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            //if (!hpl.Load(variantJsonUrl, true, _hmpForVariantsLoad))
            //{
            //    _mti.AddLogWarning("!!! Can't download variant JSON.");
            //    return false;
            //}

            //var json = JObject.Parse(hpl.Content);
            //var root = json["RetailItemComm"];

            //// https://www.ikea.com/us/en/p/hauga-upholstered-bed-frame-lofallet-beige-30490484/
            //if (root == null)
            //{

            //ProductPopulateByProductHtml(_url2ProductPageHtml[productForProcessing.Url], productForProcessing);
            string html;
            if (!_cacheCustom.Load(productForProcessing.Url, out html))
            {
                _mti.AddLogWarning(string.Format("[VariantProcessing] Variant '{0}' processing interrupt -> can't find product page html.", productForProcessing.Art));
            }
            ProductPopulateByProductHtml(html, productForProcessing);

            //}
            //else
            //{
            //    var priceToken = root.SelectToken("$.RetailItemCommPriceList.RetailItemCommPrice");
            //    if (priceToken != null && priceToken.HasValues)
            //    {
            //        if (priceToken is JArray)
            //        {
            //            bool isNewLowerPrice = false;
            //            foreach (var item in priceToken)
            //            {
            //                var price = item["Price"].Value<string>("$");
            //                var retailPriceType = item["RetailPriceType"].Value<string>("$");
            //                if (retailPriceType.Equals("RegularSalesUnitPrice", StringComparison.InvariantCultureIgnoreCase))
            //                {
            //                    if (!isNewLowerPrice)
            //                    {
            //                        var reasonCode = item["ReasonCode"];
            //                        if (reasonCode != null && reasonCode.Value<string>("$").Equals("NewLowerPrice", StringComparison.InvariantCultureIgnoreCase))
            //                        {
            //                            isNewLowerPrice = true;
            //                        }
            //                        productForProcessing.Price = price;
            //                    }
            //                }
            //                if (retailPriceType.Equals("IKEAFamilySalesUnitPrice", StringComparison.InvariantCultureIgnoreCase))
            //                {
            //                    productForProcessing.PriceOld = productForProcessing.Price;
            //                    productForProcessing.Price = price;
            //                }
            //            }
            //        }
            //        else
            //        {
            //            productForProcessing.Price = priceToken["Price"].Value<string>("$");
            //        }
            //    }

            //    var optionsToken = root.SelectToken("$.GPRCommSelectionCriteriaSelectionList.GPRCommSelectionCriteriaSelection");
            //    if (optionsToken != null && optionsToken.HasValues)
            //    {
            //        if (optionsToken is JArray)
            //        {
            //            foreach (var option in optionsToken)
            //            {
            //                var name = option[/*"SelectionCriteriaCode"*/"SelectionCriteriaName"].Value<string>("$");
            //                var val = option["SelectionCriteriaValue"].Value<string>("$");
            //                if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(val))
            //                {
            //                    if (val == "-")
            //                    {
            //                        continue;
            //                    }
            //                    var nameChars = name.Trim().ToCharArray();
            //                    nameChars[0] = char.ToUpper(nameChars[0]);
            //                    productForProcessing.AddDynamicAttribute(new string(nameChars), val.Trim());
            //                }
            //            }
            //        }
            //        else
            //        {
            //            var name = optionsToken[/*"SelectionCriteriaCode"*/"SelectionCriteriaName"].Value<string>("$");
            //            var val = optionsToken["SelectionCriteriaValue"].Value<string>("$");
            //            if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(val))
            //            {
            //                if (val != "-")
            //                {
            //                    var nameChars = name.Trim().ToCharArray();
            //                    nameChars[0] = char.ToUpper(nameChars[0]);
            //                    productForProcessing.AddDynamicAttribute(new string(nameChars), val.Trim());
            //                }
            //            }
            //        }
            //    }

            //    Dictionary<string, string> productMeasuresNames2Values = ProductMeasuresParse(root);
            //    foreach (var n2v in productMeasuresNames2Values)
            //    {
            //        productForProcessing.AddDynamicAttribute(n2v.Key, n2v.Value);
            //    }

            //    //List<PackageLegacy> packages = PackageMeasuresParse(productHtml);
            //    //PackagesAttributesProcessing(productForProcessing, packages);

            //    var productNameTokenTitle2Val = new Dictionary<string, string>();
            //    var productName = root["ProductName"];
            //    if (productName != null)
            //    {
            //        productNameTokenTitle2Val["ProductName"] = productName.Value<string>("$").Trim();
            //        //productForProcessing.Name = productName.Value<string>("$").Trim();
            //    }
            //    var productTypeName = root["ProductTypeName"];
            //    if (productTypeName != null)
            //    {
            //        productNameTokenTitle2Val["ProductTypeName"] = productTypeName.Value<string>("$").Trim();
            //        //productForProcessing.Name += " " + productTypeName.Value<string>("$").Trim();
            //    }
            //    var validDesignText = root["ValidDesignText"];
            //    if (validDesignText != null)
            //    {
            //        productNameTokenTitle2Val["ValidDesignText"] = validDesignText.Value<string>("$").Trim();
            //    }
            //    var itemMeasureReferenceTextMetric = root["ItemMeasureReferenceTextMetric"];
            //    if (itemMeasureReferenceTextMetric != null)
            //    {
            //        productNameTokenTitle2Val["ItemMeasureReferenceTextMetric"] = itemMeasureReferenceTextMetric.Value<string>("$").Trim();
            //    }

            //    productForProcessing.Name = productNameTokenTitle2Val["ProductName"] + " "
            //        + string.Join(", ", productNameTokenTitle2Val.Where(kvp => kvp.Key != "ProductName").Select(kvp => kvp.Value));

            //    productForProcessing.FullDescription = DescriptionFullBuild(root);
            //    //productForProcessing.FullDescription += DescriptionOfPackagesBuild(packages);

            //    /*
            //    var attachmentUrlTokens = root.SelectTokens("$.RetailItemCommAttachmentList..AttachmentUrl");
            //    if (attachmentUrlTokens != null)
            //    {
            //        var links = from t in attachmentUrlTokens
            //                    let url = t.Value<string>("$")
            //                    where !string.IsNullOrEmpty(url)
            //                    select TextUtils.UrlNewCompose(_domain, url);

            //        if (links.Any())
            //        {
            //            productForProcessing.AddDynamicAttribute("Инструкция", string.Join(",", links));
            //        }
            //    }
            //    */
            //}

            if (productForProcessing.ImagesCount == 0)
            {
                ImagesFill(productForProcessing);
            }

            //var avi = ProductAvailabilityInfoGet(_gps.GrabberSettings.UserParameterGet("StoreId"), artPrepared);
            //productForProcessing.Quantity = avi.Qty.ToString();
            //productForProcessing.AddDynamicAttribute("Коробки", BoxAttributeValueBuild(avi, packages));
            return true;
        }

        private void ProductPopulateByProductHtml(string productHtml, Product product)
        {
            _mti.AddLogFormatted("[ProductPopulateByProductHtml] Started. Art:{0}.", product.Art);

            if (string.IsNullOrEmpty(productHtml))
            {
                _mti.AddLogWarning("[ProductPopulateByProductHtml] Product html is empty.");
                return;
            }

            product.Name = ProductNameBuild(productHtml);

            var hDoc = new HtmlDocument();
            hDoc.LoadHtml(productHtml);

            var nodes = hDoc.DocumentNode.SelectNodes(".//div[@data-initial-props]");
            if (nodes != null)
            {
                var blockClass2InitProps = nodes.ToDictionary(n => n.Attributes["class"].Value, n => n.Attributes["data-initial-props"].Value);
                foreach (var block in blockClass2InitProps)
                {
                    var html = HttpUtility.HtmlDecode(block.Value);
                    var j = JObject.Parse(html);

                    if (block.Key.IndexOf("pip-price-package", StringComparison.InvariantCultureIgnoreCase) >= 0)
                    {
                        var price = j["price"]["mainPriceProps"]["price"];
                        product.Price = string.Format("{0}{1}{2}", price.Value<string>("integer"), j["price"]["mainPriceProps"].Value<string>("separator"), price.Value<string>("decimals"));
                    }
                    else if (block.Key.IndexOf("revamp-product-styles", StringComparison.InvariantCultureIgnoreCase) >= 0)
                    {
                        var variant = JsonConvert.DeserializeObject<ModelsIkea.Variations.Variation>(html);
                        var options = variant.allOptions.Count > variant.options.Count ? variant.allOptions : variant.options;
                        foreach (var opt in options)
                        {
                            if (opt.isSelected && !string.IsNullOrEmpty(opt.title))
                            {
                                product.AddDynamicAttribute(variant.title, opt.title);
                            }
                        }
                    }
                    else if (block.Key.IndexOf("product-variation-section", StringComparison.InvariantCultureIgnoreCase) >= 0)
                    {
                        var variations = j.SelectToken("variations");
                        if (variations != null)
                        {
                            variations = variations as JArray;
                            if (variations != null && variations.HasValues)
                            {
                                foreach (JObject v in variations)
                                {
                                    var variant = v.ToObject<ModelsIkea.Variations.Variation>();
                                    var options = variant.allOptions.Count > variant.options.Count ? variant.allOptions : variant.options;
                                    foreach (var opt in options)
                                    {
                                        if (opt.isSelected && !string.IsNullOrEmpty(opt.title))
                                        {
                                            product.AddDynamicAttribute(variant.title, opt.title);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    else if (block.Key.IndexOf("product-information-section", StringComparison.InvariantCultureIgnoreCase) >= 0)
                    {
                        var dimName2Val = new Dictionary<string, string>();
                        try
                        {
                            var dimensionProps = j.Value<JObject>("dimensionProps");
                            if (dimensionProps != null)
                            {
                                var dims = dimensionProps.Value<JArray>("dimensions");
                                if (dims != null && dims.HasValues)
                                {
                                    foreach (JObject d in dims)
                                    {
                                        var n = d.Value<string>("name");
                                        var v = d.Value<string>("measure");
                                        if (!string.IsNullOrEmpty(n) && !string.IsNullOrEmpty(v))
                                        {
                                            dimName2Val[n.Trim()] = v.Trim();
                                        }
                                    }
                                }
                            }
                        }
                        catch { }

                        foreach (var n2v in dimName2Val)
                        {
                            product.AddDynamicAttribute(n2v.Key, n2v.Value);
                        }

                        var pkgs = PackageMeasuresParse(html, _ikeaSettings.Region == Region.EN ? MeasureSystem.US_STANDARD : MeasureSystem.METRIC);

                        if (_ikeaSettings.Region == Region.EN)
                        {
                            var w = PackageHelper.WeightTotalUsStandardValCalc(pkgs);
                            if (_ikeaSettings.PackagesWeightTotalAsAttribute)
                            {
                                product.AddDynamicAttribute("Package weight", string.Format("{0}{1}", w.Key > 0 ? w.Key.ToString() + " lb " : "",
                                    w.Value > 0 ? w.Value.ToString() + " oz" : ""));
                            }
                        }
                        else
                        {
                            if (_ikeaSettings.ProductSizesAsPackagesSizesTotal)
                            {
                                product.Weight = PackageHelper.MeasureTotalValCalc(pkgs, MeasureType.WEIGHT, _ikeaSettings.Region).ToString();
                                product.Height = PackageHelper.MeasureTotalValCalc(pkgs, MeasureType.HEIGHT, _ikeaSettings.Region).ToString();
                                product.Width = PackageHelper.MeasureTotalValCalc(pkgs, MeasureType.WIDTH, _ikeaSettings.Region).ToString();
                                product.Depth = PackageHelper.MeasureTotalValCalc(pkgs, MeasureType.LENGTH, _ikeaSettings.Region).ToString();
                            }
                        }

                        product.FullDescription = DescriptionFullBuild(j.Value<JObject>("productDetailsProps"), dimensions: dimName2Val);
                    }
                }
            }
        }

        private string ProductNameBuild(string productHtml)
        {
            if (!string.IsNullOrEmpty(productHtml))
            {
                var hdoc = new HtmlDocument();
                hdoc.LoadHtml(productHtml);
                //var node_1 = hdoc.DocumentNode.SelectSingleNode(".//div[@id='name']");
                //string n1 = NodeValGet(node_1, ".//strong", @"[\r|\t|\n|\s]{2,}[--->] ");
                //var node_2 = hdoc.DocumentNode.SelectSingleNode(".//h1");
                //string n2 = NodeValGet(node_2, ".//strong", @"</div><div[^>]*?>[--->] [next]<[^>]*?>[next]\s{2,}[--->] ");
                //return string.Format("{0} {1}", n1.Trim(), HttpUtility.HtmlDecode(n2.Trim())).Trim();
                var node_2 = hdoc.DocumentNode.SelectSingleNode(".//h1");
                var parts = new List<string>();
                if (node_2 != null)
                {
                    foreach (var n in node_2.ChildNodes)
                    {
                        if (!string.IsNullOrEmpty(n.InnerText))
                        {
                            parts.Add(n.InnerText.Trim());
                        }
                    }
                }
                return HttpUtility.HtmlDecode(string.Join(" ", parts));
            }
            return string.Empty;
        }
        private string NodeValGet(HtmlNode node, string removeAllInnerNodesXpath = null, string replaceInnerHtmlRegex = null)
        {
            if (node != null)
            {
                if (!string.IsNullOrEmpty(removeAllInnerNodesXpath))
                {
                    var rems = node.SelectNodes(removeAllInnerNodesXpath);
                    if (rems != null)
                    {
                        foreach (var r in rems)
                        {
                            r.Remove();
                        }
                    }
                    var val = node.InnerHtml;
                    if (!string.IsNullOrEmpty(replaceInnerHtmlRegex))
                    {
                        var patterns = UtilSmall.GetParts(replaceInnerHtmlRegex);
                        foreach (var p in patterns)
                        {
                            var fromTo = UtilSmall.GetParts(p, "[--->]");
                            if (fromTo.Length == 1)
                            {
                                fromTo = new string[2] { fromTo[0], "" };
                            }
                            val = Regex.Replace(val, fromTo[0], fromTo[1]);
                        }
                    }
                    return val;
                }
            }
            return string.Empty;
        }

        private string BoxAttributeValueBuild(AvailabilityInfo avi, List<PackageLegacy> packages, string propDelim = ";", string itemDelim = "[next]")
        {
            var vals = from s in avi.SalesLocations
                       join p in packages on s.ItemNo equals p.ItemNo
                       select string.Format("product_set_name:{1}{0}product_count:{2}{0}product_set_ean:{3}{0}row:{4}{0}place:{5}",
                       propDelim,
                       p.ProductName,
                       p.PackagesQty,
                       p.ItemNo,
                       s.Aisle,
                       s.Bin);

            return string.Join(itemDelim, vals);
        }

        private string DescriptionOfPackagesBuild(List<PackageLegacy> packages)
        {
            var sBuilder = new StringBuilder();
            sBuilder.Append("<div><strong>Packaging</strong></div>");
            if (packages.Count > 1)
            {
                sBuilder.AppendFormat("<h5>This product comes as {0} packages.</h5>", packages.Sum(p => p.PackagesQty).ToString());
            }

            string idCurrent = "default";
            foreach (var pkg in packages)
            {
                sBuilder.Append("<div>");
                string id = IDCreate(pkg.ItemNo);
                if (idCurrent != id)
                {
                    idCurrent = id;
                    sBuilder.AppendFormat("<div><h5>{0}</h5><p>{1}</p><p><span>Article Number</span><br/><span>{2}</span></p></div>",
                        pkg.ProductName, pkg.ProductTypeName, id);
                }
                else
                { sBuilder.Append("<br/>"); }

                foreach (var measureN2V in pkg.MeasuresName2Val)
                {
                    sBuilder.AppendFormat("<div>{0}: {1}</div>", PackageHelper.PackageSizeNameTranslate(measureN2V.Key), measureN2V.Value);
                }
                sBuilder.AppendFormat("<div>{0}: {1}</div>", "Package(s)", pkg.PackagesQty.ToString());

                sBuilder.Append("</div>");
            }
            return sBuilder.ToString();
        }
        private void PackagesAttributesProcessing(Product product, List<PackageLegacy> packages)
        {
            /*
                     *  http://crm.catalogloader.com/issues/35219
                        4. Вес товара и габариты упаковки
                        Пояснение:
                         — Кол-во товаров в упаковке сделать характеристикой; Ок.
                         — Расчёт габаритов;
                        Ширина. Берём максимальное значение
                        Высота. Суммируем
                        Длина. Берём максимальное значение
                         Полученное выводим в характеристики.
                         Это будет возможно, если на всех карточках товарах одинаково оформлены габариты
                        Пробуем. Получится — хорошо, не получится — ладно.
                        — Расчёт объёма
                        Рассчитать объём и вывести характеристикой Напишите формулу расчета.
                        Формула расчёта объёма: 
                        Полученные габариты суммируем:
                        58*21*88 = 107 184 см3. 
                        Получаем объём в м3. 107 184 * 0,000001 = 0,107184 м3
                     */
            product.AddDynamicAttribute("Кол-во упаковок", packages.Sum(p => p.PackagesQty).ToString());
            double weightTotal = PackageHelper.MeasureTotalValCalc(packages, MeasureType.WEIGHT, _ikeaSettings.Region);
            product.SetAttributeValue("SYS_PRODUCT_WEIGHT", string.Format("{0:N2}", weightTotal));

            double heightTotal = PackageHelper.MeasureTotalValCalc(packages, MeasureType.HEIGHT, _ikeaSettings.Region);
            product.AddDynamicAttribute(PackageHelper.PackageSizeNameTranslate(MeasureType.HEIGHT.ToString()), string.Format("{0:N2}", heightTotal));

            double widthMax = PackageHelper.MeasureMaxValueGet(packages, MeasureType.WIDTH, _ikeaSettings.Region);
            product.AddDynamicAttribute(PackageHelper.PackageSizeNameTranslate(MeasureType.WIDTH.ToString()), string.Format("{0:N2}", widthMax));

            double lengthMax = PackageHelper.MeasureMaxValueGet(packages, MeasureType.LENGTH, _ikeaSettings.Region);
            product.AddDynamicAttribute(PackageHelper.PackageSizeNameTranslate(MeasureType.LENGTH.ToString()), string.Format("{0:N2}", lengthMax));

            double pakageVal = heightTotal * widthMax * lengthMax * 0.000001;
            product.AddDynamicAttribute("Объем, м3", string.Format("{0:N2}", pakageVal));
        }

        private List<PackageLegacy> PackageMeasuresParse(string productHtml, MeasureSystem measureSystem = MeasureSystem.METRIC)
        {
            _mti.AddLogInfo("[PackageMeasuresParse] Started.");
            var packages = new List<PackageLegacy>();
            if (!string.IsNullOrEmpty(productHtml))
            {
                try
                {
                    //string jsonStr = HtmlValueGet(productHtml, ".//div[contains(@class,'js-product-information-section range-revamp-product-information-section')]", "data-initial-props");
                    string jsonStr = productHtml;

                    if (!string.IsNullOrEmpty(jsonStr))
                    {
                        jsonStr = HttpUtility.HtmlDecode(jsonStr);
                        var json = JsonConvert.DeserializeObject<ModelsIkea.ProductProps.Props>(jsonStr);
                        var packaging = json.DimensionPropsObject != null && json.DimensionPropsObject.packaging != null ? json.DimensionPropsObject.packaging : null;

                        if (packaging == null)
                        {
                            _mti.AddLogInfo("[PackageMeasuresParse] json.DimensionPropsObject.packaging == null ? TRUE");
                            packaging = json.productDetailsProps != null && json.productDetailsProps.accordionObject != null ? json.productDetailsProps.accordionObject.packaging : null;
                            if (packaging != null)
                            {
                                _mti.AddLogInfo("[PackageMeasuresParse] packaging IS json.productDetailsProps.accordionObject.packaging");
                            }
                        }
                        else
                        {
                            _mti.AddLogInfo("[PackageMeasuresParse] packaging IS json.DimensionPropsObject.packaging");
                        }

                        if (packaging != null && packaging.contentProps != null && packaging.contentProps.packages != null)
                        {
                            foreach (var pck in packaging.contentProps.packages)
                            {
                                if (pck.measurements == null || pck.measurements.Length == 0)
                                {
                                    continue;
                                }
                                foreach (var m in pck.measurements)
                                {
                                    PackageLegacy package = null;
                                    var m2vr = new Dictionary<string, string>();
                                    foreach (var msrm in m)
                                    {
                                        if (string.IsNullOrEmpty(msrm.label) || string.IsNullOrEmpty(msrm.value))
                                        {
                                            continue;
                                        }
                                        //if (package == null)
                                        //{
                                        //    package = new PackageLegacy();
                                        //}
                                        m2vr[msrm.label.ToUpperInvariant()] = msrm.value;
                                        //string valNormal = Regex.Replace(msrm.value, @"\D+?$", "");
                                        //package.MeasuresName2Val[MeasureNameLegacyGet(msrm.label)] = UtilSmall.ConvertToDouble(valNormal);

                                    }
                                    if (package == null)
                                    {
                                        package = new PackageLegacy(m2vr, measureSystem);
                                    }
                                    if (package != null)
                                    {
                                        package.PackagesQty = pck.quantity.value;
                                        package.ItemNo = pck.articleNumber.value;
                                        package.ProductName = pck.name;
                                        package.ProductTypeName = pck.typeName;

                                        packages.Add(package);
                                    }
                                }
                            }
                        }
                        else
                        {
                            _mti.AddLogInfo("[PackageMeasuresParse] json.productDetailsProps.accordionObject.packaging == null ? TRUE");
                        }
                    }
                }
                catch (Exception ex)
                {
                    _mti.AddLogError("PackageMeasuresParse() failed.", ex);
                }
            }
            _mti.AddLogInfo("[PackageMeasuresParse] Finished.");
            return packages;
        }
        private string MeasureNameLegacyGet(string nameModern)
        {
            string ptrn = nameModern.ToUpperInvariant();
            switch (ptrn)
            {
                case "ШИРИНА":
                    return "WIDTH";
                case "ВЫСОТА":
                    return "HEIGHT";
                case "ДЛИНА":
                    return "LENGTH";
                case "ВЕС":
                    return "WEIGHT";
                case "ДИАМЕТР":
                    return "DIAMETER";
                default:
                    return nameModern.ToUpperInvariant();
            }
        }
        private string HtmlValueGet(string html, string nodeXpath, string attributeName = null, bool plainTxt = false)
        {
            string val = "";
            try
            {
                var hdoc = new HtmlDocument();
                hdoc.LoadHtml(html);
                var node = hdoc.DocumentNode.SelectSingleNode(nodeXpath);
                if (node != null)
                {
                    if (string.IsNullOrEmpty(attributeName))
                    {
                        val = plainTxt ? node.InnerText : node.OuterHtml;
                    }
                    else
                    {
                        val = node.GetAttributeValue(attributeName, "");
                    }
                }
            }
            catch (Exception ex)
            {
                _mti.AddLogError(ex);
            }
            return val;
        }

        private void ImagesFill(Product productForProcessing)
        {
            string imgsJson = productForProcessing.GetAttributeValue("MEDIA_GRID");
            if (string.IsNullOrEmpty(imgsJson))
            {
                string html = "";
                /*
                if (!_url2ProductPageHtml.TryGetValue(productForProcessing.Url, out html))
                {
                    var hplImgs = _gps.Proxy.GetHtmlPageLoaderEmpty();
                    if (hplImgs.Load(productForProcessing.Url))
                    {
                        _url2ProductPageHtml[productForProcessing.Url] = html = hplImgs.Content;
                    }
                }
                */
                if (!_cacheCustom.Load(productForProcessing.Url, out html))
                {
                    var hplImgs = _gps.Proxy.GetHtmlPageLoaderEmpty();
                    hplImgs.Load(productForProcessing.Url);
                }

                if (!string.IsNullOrEmpty(html))
                {
                    var hdoc = new HtmlDocument();
                    hdoc.LoadHtml(html);
                    //imgsJson = TextUtils.GetHtmlValue(hdoc, ".//div[contains(@class,'js-range-media-grid range-revamp-media-grid')]", "data-initial-props", true, 0);
                    imgsJson = TextUtils.GetHtmlValue(hdoc, ".//div[contains(@class,'js-range-media-grid')]", "data-initial-props", true, 0);
                }
            }
            if (!string.IsNullOrEmpty(imgsJson))
            {
                var imgs = new List<ImageClp>();
                var mediaLst = new List<ModelsIkea.Media.MediaListBase>();
                try
                {
                    var j = JsonConvert.DeserializeObject<ModelsIkea.Media.MediaGrid>(imgsJson);
                    mediaLst = j.fullMediaList.Any() ? j.fullMediaList.ToList<ModelsIkea.Media.MediaListBase>()
                                                     : j.mediaList.ToList<ModelsIkea.Media.MediaListBase>();
                }
                catch (Exception ex)
                {
                    _mti.AddLogError(ex);
                }
                foreach (var media in mediaLst)
                {
                    if (!media.type.Equals("image", StringComparison.InvariantCultureIgnoreCase)
                        || media.content == null
                        || string.IsNullOrEmpty(media.content.imageFileName)
                        || string.IsNullOrEmpty(media.content.url))
                    {
                        continue;
                    }

                    imgs.Add(new ImageClp() { FileName = media.content.imageFileName, Url = media.content.url });
                }
                productForProcessing.ImageSafeAdd(imgs);
            }
        }
        private Dictionary<string, string> ProductMeasuresParse(JToken root)
        {
            var measuresNames2Vals = new Dictionary<string, string>();
            var itemMeasureToken = root.SelectToken("$.RetailItemCommMeasureList.RetailItemCommMeasure");
            if (itemMeasureToken != null && itemMeasureToken.HasValues)
            {
                if (itemMeasureToken is JArray)
                {
                    foreach (var pkg in itemMeasureToken)
                    {
                        var nameOrig = pkg["ItemMeasureTypeName"].Value<string>("$");
                        var val = pkg["ItemMeasureTextMetric"].Value<string>("$");
                        if (string.IsNullOrEmpty(nameOrig) || string.IsNullOrEmpty(val))
                        {
                            continue;
                        }
                        measuresNames2Vals[nameOrig.Trim()] = val.Trim();
                    }
                }
                else
                {
                    var nameOrig = itemMeasureToken["ItemMeasureTypeName"].Value<string>("$");
                    var val = itemMeasureToken["ItemMeasureTextMetric"].Value<string>("$");
                    if (!string.IsNullOrEmpty(nameOrig) || !string.IsNullOrEmpty(val))
                    {
                        measuresNames2Vals[nameOrig.Trim()] = val.Trim();
                    }
                }
            }
            return measuresNames2Vals;
        }

        internal class AvailabilityInfo
        {
            internal string ItemNo { get; set; }
            internal int Qty { get; set; }
            internal List<SalesLocationInfo> SalesLocations { get; set; }
            internal AvailabilityInfo()
            {
                Qty = -1;
                SalesLocations = new List<SalesLocationInfo>();
            }
        }

        internal class SalesLocationInfo
        {
            internal string ItemNo { get; set; }
            internal string Aisle { get; set; }
            internal string Bin { get; set; }
        }

        internal class PackageLegacy
        {
            internal Dictionary<string, double> MeasuresName2Val { get; private set; }
            internal Dictionary<string, string> MeasuresName2ValRaw { get; private set; }
            internal string ProductName { get; set; }
            internal string ItemNo { get; set; }
            internal int PackagesQty { get; set; }
            public string ProductTypeName { get; internal set; }
            public MeasureSystem MeasureSystem { get; private set; }

            internal PackageLegacy(Dictionary<string, string> measuresName2ValRaw, MeasureSystem measureSystem = MeasureSystem.METRIC)
            {
                PackagesQty = 1;
                MeasuresName2Val = new Dictionary<string, double>();
                MeasuresName2ValRaw = measuresName2ValRaw;
                MeasureSystem = measureSystem;
                Init();
            }

            private void Init()
            {
                if (MeasuresName2ValRaw == null)
                {
                    MeasuresName2ValRaw = new Dictionary<string, string>();
                }
                foreach (var n2rv in MeasuresName2ValRaw)
                {
                    if (MeasureSystem == MeasureSystem.METRIC)
                    {
                        string valNormal = Regex.Replace(n2rv.Value, @"\D+?$", "");
                        MeasuresName2Val[n2rv.Key] = UtilSmall.ConvertToDouble(valNormal);
                    }
                }
            }

            internal string ToHtml()
            {
                var sBuilder = new StringBuilder("<package>");
                foreach (var n2v in MeasuresName2Val)
                {
                    string nameToLower = n2v.Key.ToLowerInvariant();
                    double valueNormal = n2v.Value;
                    if (nameToLower != "weight")
                    {
                        valueNormal = valueNormal / 100;
                    }
                    sBuilder.AppendFormat("<{0}>{1}</{0}>", nameToLower, valueNormal.ToString());
                }
                sBuilder.AppendFormat("<{0}>{1}</{0}>", "sku", ItemNo);
                sBuilder.AppendFormat("<{0}>{1}</{0}>", "quantity", PackagesQty);
                sBuilder.Append("</package>");
                return sBuilder.ToString();
            }
        }

        internal enum MeasureType
        {
            WIDTH, HEIGHT, LENGTH, WEIGHT, DIAMETER
        }
        internal enum MeasureSystem
        { METRIC, US_STANDARD }

        internal static class PackageHelper
        {
            internal static double MeasureMaxValueGet(List<PackageLegacy> packages, MeasureType type, Region r)
            {
                double valMax = 0;
                foreach (var p in packages)
                {
                    double val;
                    //if (p.MeasuresName2Val.TryGetValue(type.ToString(), out val))
                    if (p.MeasuresName2Val.TryGetValue(PackageSizeNameTranslate(type, r), out val))
                    {
                        if (valMax < val)
                        {
                            valMax = val;
                        }
                    }
                }
                return valMax;
            }
            internal static double MeasureTotalValCalc(List<PackageLegacy> packages, MeasureType type, Region r)
            {
                double valTotal = 0;
                foreach (var p in packages)
                {
                    double val;
                    //if (p.MeasuresName2Val.TryGetValue(type.ToString(), out val))
                    if (p.MeasuresName2Val.TryGetValue(PackageSizeNameTranslate(type, r), out val))
                    {
                        valTotal += val * p.PackagesQty;
                    }
                }
                return valTotal;
            }
            internal static KeyValuePair<int, int> WeightTotalUsStandardValCalc(List<PackageLegacy> packages)
            {
                int lbs = 0;
                int ozs = 0;
                string ptrn = @"((?<lb>\d+)\slb)?\s*((?<oz>\d+)\s*oz)?";
                foreach (var p in packages)
                {
                    if (p.MeasureSystem == MeasureSystem.US_STANDARD)
                    {
                        string val;
                        if (p.MeasuresName2ValRaw.TryGetValue(PackageSizeNameTranslate(MeasureType.WEIGHT, Region.EN), out val))
                        {
                            if (Regex.IsMatch(val, ptrn, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
                            {
                                var grps = Regex.Match(val, ptrn, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase).Groups;
                                if (grps["lb"] != null && !string.IsNullOrEmpty(grps["lb"].Value))
                                {
                                    lbs += int.Parse(grps["lb"].Value);
                                }
                                if (grps["oz"] != null && !string.IsNullOrEmpty(grps["oz"].Value))
                                {
                                    ozs += int.Parse(grps["oz"].Value);
                                    lbs += ozs / 16; // 1 lb = 16 oz
                                    ozs %= 16;
                                }
                            }
                        }
                    }
                }

                return new KeyValuePair<int, int>(lbs, ozs);
            }
            internal static string PackageSizeNameTranslate(string source)
            {
                if (string.IsNullOrEmpty(source))
                {
                    return string.Empty;
                }
                switch (source)
                {
                    case "WEIGHT":
                        return "Вес упаковки, кг";
                    case "HEIGHT":
                        return "Высота упаковки, см";
                    case "LENGTH":
                        return "Длина упаковки, см";
                    case "WIDTH":
                        return "Ширина упаковки, см";
                    case "DIAMETER":
                        return "Диаметр, см";
                    default:
                        return string.Empty;
                }
            }

            private static Dictionary<Region, Dictionary<MeasureType, string>> _region2Measure2Type = new Dictionary<Region, Dictionary<MeasureType, string>>()
        {
            {
                Region.PL, new Dictionary<MeasureType, string>()
                {
                    { MeasureType.WEIGHT, "WAGA" },
                    { MeasureType.WIDTH, "SZEROKOŚĆ" },
                    { MeasureType.DIAMETER, "ŚREDNICA" },
                    { MeasureType.HEIGHT, "WYSOKOŚĆ" },
                    { MeasureType.LENGTH, "DŁUGOŚĆ" }
                }
            },
            {
                Region.EN, new Dictionary<MeasureType, string>()
                {
                    { MeasureType.WEIGHT, "WEIGHT" },
                    { MeasureType.WIDTH, "WIDTH" },
                    { MeasureType.DIAMETER, "DIAMETER" },
                    { MeasureType.HEIGHT, "HEIGHT" },
                    { MeasureType.LENGTH, "LENGTH" }
                }
            },
            {
                Region.RU, new Dictionary<MeasureType, string>()
                {
                    { MeasureType.WEIGHT, "ВЕС" },
                    { MeasureType.WIDTH, "ШИРИНА" },
                    { MeasureType.DIAMETER, "ДИАМЕТР" },
                    { MeasureType.HEIGHT, "ВЫСОТА" },
                    { MeasureType.LENGTH, "ДЛИННА" }
                }
            }
        };
            internal static string PackageSizeNameTranslate(MeasureType mt, Region r)
            {
                Dictionary<MeasureType, string> measure2LocalName;
                if (_region2Measure2Type.TryGetValue(r, out measure2LocalName))
                {
                    string localName;
                    if (measure2LocalName.TryGetValue(mt, out localName))
                    {
                        return localName;
                    }
                }
                return string.Empty;
            }
        }

        private Dictionary<string, double> RetailItemCommPackageMeasureParse(JToken root)
        {
            var measureName2Val = new Dictionary<string, double>();
            if (root is JArray)
            {
                foreach (var pkg in root)
                {
                    var nameOrig = pkg["PackageMeasureType"].Value<string>("$");
                    var val = pkg["PackageMeasureTextMetric"].Value<string>("$");
                    val = Regex.Replace(val, @"\D+?$", "");
                    if (string.IsNullOrEmpty(nameOrig) || string.IsNullOrEmpty(val))
                    {
                        continue;
                    }
                    measureName2Val[nameOrig] = UtilSmall.ConvertToDouble(val);
                }
            }
            else
            {
                var nameOrig = root["PackageMeasureType"].Value<string>("$");
                var val = root["PackageMeasureTextMetric"].Value<string>("$");
                val = Regex.Replace(val, @"\D+?$", "");
                if (!string.IsNullOrEmpty(nameOrig) || !string.IsNullOrEmpty(val))
                {
                    measureName2Val[nameOrig] = UtilSmall.ConvertToDouble(val);
                }
            }
            return measureName2Val;
        }

        private string DescriptionFullBuild(JToken root, string packagesHtml = null, Dictionary<string, string> dimensions = null)
        {
            var sBuilder = new StringBuilder();
            JObject detailes = null;
            var benefitsToken = root.SelectToken("$.RetailItemCustomerBenefitList.RetailItemCustomerBenefit");
            if (benefitsToken != null && benefitsToken.HasValues)
            {
                sBuilder.Append("<div><strong>Product details</strong></div>");
                if (benefitsToken is JArray)
                {
                    foreach (var b in benefitsToken)
                    {
                        sBuilder.AppendFormat("<div>{0}</div>", b["CustomerBenefitText"].Value<string>("$"));
                    }
                }
                else
                {
                    sBuilder.AppendFormat("<div>{0}</div>", benefitsToken["CustomerBenefitText"].Value<string>("$"));
                }
            }
            else
            {
                detailes = root.Value<JObject>("productDescriptionProps");
                if (detailes != null)
                {
                    sBuilder.Append("<div><strong>Product details</strong></div>");
                    var prfs = detailes.Value<JArray>("paragraphs");
                    if (prfs != null && prfs.HasValues)
                    {
                        foreach (var pr in prfs)
                        {
                            sBuilder.AppendFormat("<div>{0}</div>", pr.Value<string>());
                        }
                    }
                    var disr = detailes.Value<string>("designerName");
                    if (!string.IsNullOrEmpty(disr))
                    {
                        sBuilder.AppendFormat("<div><strong>Designer</strong></div><div>{0}</div>", disr);
                    }
                }
            }

            var designerObj = root.SelectToken("$.DesignerNameComm");
            if (designerObj != null)
            {
                sBuilder.AppendFormat("<div><strong>Designer</strong></div><div>{0}</div>", designerObj.Value<string>("$"));
            }

            if (packagesHtml != null)
            {
                sBuilder.Append(packagesHtml);
            }

            var itemMeasureToken = root.SelectToken("$.RetailItemCommMeasureList.RetailItemCommMeasure");
            if (itemMeasureToken != null && itemMeasureToken.HasValues)
            {
                sBuilder.Append("<div><strong>Product size</strong></div>");
                if (itemMeasureToken is JArray)
                {
                    foreach (var pkg in itemMeasureToken)
                    {
                        var nameOrig = pkg["ItemMeasureTypeName"].Value<string>("$");
                        var val = pkg["ItemMeasureTextMetric"].Value<string>("$");
                        if (string.IsNullOrEmpty(nameOrig) || string.IsNullOrEmpty(val))
                        {
                            continue;
                        }
                        sBuilder.AppendFormat("<div>{0}: {1}</div>", Translate(nameOrig), val);
                    }
                }
                else
                {
                    var nameOrig = itemMeasureToken["ItemMeasureTypeName"].Value<string>("$");
                    var val = itemMeasureToken["ItemMeasureTextMetric"].Value<string>("$");
                    if (!string.IsNullOrEmpty(nameOrig) || !string.IsNullOrEmpty(val))
                    {
                        sBuilder.AppendFormat("<div>{0}: {1}</div>", Translate(nameOrig), val);
                    }
                }
            }
            else if (dimensions != null)
            {
                foreach (var d in dimensions)
                {
                    sBuilder.AppendFormat("<div>{0}: {1}</div>", d.Key, d.Value);
                }
            }

            JObject accordion = detailes != null ? root.Value<JObject>("accordionObject") : null;
            var environmentTokens = root.SelectTokens("$.RetailItemCustomerEnvironmentList..RetailItemEnvironmentText..EnvironmentText");
            if (environmentTokens != null)
            {
                sBuilder.Append("<div><strong>Environment</strong></div>");
                foreach (var e in environmentTokens)
                {
                    sBuilder.AppendFormat("<div>{0}</div>", e.Value<string>("$"));
                }
            }
            else if (detailes != null)
            {
                if (accordion != null)
                {
                    var envData = accordion.SelectToken("sustainabilityAndEnvironment.contentProps.environmentalData");
                    if (envData != null)
                    {
                        var envs = envData as JArray;
                        if (envs != null && envs.HasValues)
                        {
                            sBuilder.Append("<div><strong>Sustainability & environment</strong></div>");
                            foreach (var e in environmentTokens)
                            {
                                var txts = e.Value<JArray>("texts");
                                if (txts != null && txts.HasValues)
                                {
                                    foreach (var t in txts)
                                    {
                                        sBuilder.AppendFormat("<div>{0}</div>", t.Value<string>());
                                    }
                                }

                            }
                        }
                    }
                }
            }

            sBuilder.Append("<div><strong>Materials & care</strong></div>");
            var materialsRoot = root.SelectToken("$.RetailItemCustomerMaterialList.RetailItemCustomerMaterial");
            if (materialsRoot == null && accordion != null)
            {
                var m = accordion.SelectToken("materialsAndCare.contentProps.materials");
                if (m != null && m.HasValues)
                {
                    materialsRoot = m.First.SelectToken("materials");
                }
            }
            string materialsStr = MaterialStringBuild(materialsRoot);
            sBuilder.Append(materialsStr);

            var caresToken = root.SelectToken("$.RetailItemCareInstructionList.RetailItemCareInstruction");
            if (caresToken != null)
            {
                //sBuilder.Append("<div><strong>Инструкция по уходу</strong></div>");
                if (caresToken is JArray)
                {
                    foreach (var item in caresToken)
                    {
                        var caresHeaderObj = item["CareInstructionHeader"];
                        if (caresHeaderObj != null && caresHeaderObj.HasValues)
                        {
                            sBuilder.AppendFormat("<h5>{0}</h5>", caresHeaderObj.Value<string>("$"));
                        }
                        var caresTexts = item.SelectTokens("$..CareInstructionText");
                        if (caresTexts != null)
                        {
                            foreach (var txt in caresTexts)
                            {
                                sBuilder.AppendFormat("<div>{0}</div>", txt.Value<string>("$"));
                            }
                        }
                    }
                }
                else
                {
                    var caresHeaderObj = caresToken["CareInstructionHeader"];
                    if (caresHeaderObj != null && caresHeaderObj.HasValues)
                    {
                        sBuilder.AppendFormat("<h5>{0}</h5>", caresHeaderObj.Value<string>("$"));
                    }
                    var caresTexts = caresToken.SelectTokens("$..CareInstructionText");
                    if (caresTexts != null)
                    {
                        foreach (var txt in caresTexts)
                        {
                            sBuilder.AppendFormat("<div>{0}</div>", txt.Value<string>("$"));
                        }
                    }
                }
            }
            else if (accordion != null)
            {
                var careInstructions = accordion.SelectToken("materialsAndCare.contentProps.careInstructions");
                if (careInstructions != null && careInstructions.HasValues)
                {
                    var txts = careInstructions.First().Value<JArray>("texts");
                    if (txts != null && txts.HasValues)
                    {
                        foreach (var t in txts)
                        {
                            sBuilder.AppendFormat("<div>{0}</div>", t);
                        }
                    }
                }
            }
            return sBuilder.ToString();
        }

        private string MaterialStringBuild(JToken materialsRoot)
        {
            var sBuilder = new StringBuilder();
            if (materialsRoot != null && materialsRoot.HasValues)
            {
                //sBuilder.Append("<div><strong>Материалы</strong></div>");
                if (materialsRoot is JArray)
                {
                    foreach (var item in materialsRoot)
                    {
                        string materialTokenString = MaterialTokenToString(item);
                        sBuilder.Append(materialTokenString);
                    }
                }
                else
                {
                    string materialTokenString = MaterialTokenToString(materialsRoot);
                    sBuilder.Append(materialTokenString);
                }
            }

            return sBuilder.ToString();
        }

        private string MaterialTokenToString(JToken t)
        {
            var sBuilder = new StringBuilder();
            var obj = t as JObject;
            if (obj != null && obj.HasValues)
            {
                var productTypeText = obj["ProductTypeText"];
                if (productTypeText != null)
                {
                    sBuilder.AppendFormat("<div>{0}</div>", productTypeText.Value<string>("$"));
                }
                var retailItemPartMaterial = obj.SelectToken("$..RetailItemPartMaterial");
                if (retailItemPartMaterial != null && retailItemPartMaterial.HasValues)
                {
                    if (retailItemPartMaterial is JArray)
                    {
                        foreach (var item in retailItemPartMaterial)
                        {
                            var materialText = item["MaterialText"];
                            var partText = item["PartText"];
                            if (partText != null)
                            {
                                sBuilder.AppendFormat("<div>{0} {1}</div>", partText.Value<string>("$"), materialText.Value<string>("$"));
                            }
                            else
                            {
                                sBuilder.AppendFormat("<div>{0}</div>", materialText.Value<string>("$"));
                            }
                        }
                    }
                    else
                    {

                        var materialText = retailItemPartMaterial["MaterialText"];
                        var partText = retailItemPartMaterial["PartText"];
                        if (partText != null)
                        {
                            sBuilder.AppendFormat("<div>{0} {1}</div>", partText.Value<string>("$"), materialText.Value<string>("$"));
                        }
                        else
                        {
                            sBuilder.AppendFormat("<div>{0}</div>", materialText.Value<string>("$"));
                        }
                    }
                }
                else
                {
                    string part = obj.Value<string>("part");
                    string material = obj.Value<string>("material");
                    if (!string.IsNullOrEmpty(part) && !string.IsNullOrEmpty(material))
                    {
                        sBuilder.AppendFormat("<div>{0} {1}</div>", part, material);
                    }
                    else if (!string.IsNullOrEmpty(material))
                    {
                        sBuilder.AppendFormat("<div>{0}</div>", material);
                    }
                }
            }
            return sBuilder.ToString();
        }

        private string Translate(string nameOrig)
        {
            if (nameOrig.Equals("WIDTH"))
            {
                return "Width";
            }
            else if (nameOrig.Equals("HEIGHT"))
            {
                return "Height";
            }
            else if (nameOrig.Equals("LENGTH"))
            {
                return "Length";
            }
            else if (nameOrig.Equals("WEIGHT"))
            {
                return "Weight";
            }
            else
            {
                return nameOrig;
            }
        }
        internal class Variant
        {
            //public string DataLinkId { get; set; }
            //public string DataUrl { get; set; }
            public string Id { get; set; }
            public string Url { get; set; }
            public bool IsSelected { get; set; }
            public double Price { get; set; }
        }

        private void ProductImagesNumberTruncate(Product p, int maxImgsNumber)
        {
            var imgs = p.ImagesGet();
            if (imgs.Count < maxImgsNumber + 1)
            {
                return;
            }
            p.ImagesClear();
            for (int i = 0; i < maxImgsNumber; i++)
            {
                p.ImageAdd(imgs.ElementAt(i).Key, imgs.ElementAt(i).Value);
            }
        }

        private Product HtmlBlocksClear(Product p)
        {
            if (p == null)
            {
                return null;
            }

            if (!string.IsNullOrEmpty(p.HtmlBlockProperties))
            {
                p.HtmlBlockProperties = "";
            }
            if (!string.IsNullOrEmpty(p.HtmlBlockCombinations))
            {
                p.HtmlBlockCombinations = "";
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("SYS_IMAGES_ALL")))
            {
                p.SetAttributeValue("SYS_IMAGES_ALL", "");
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("SYS_ALL_COMB_ATTR")))
            {
                p.SetAttributeValue("SYS_ALL_COMB_ATTR", "");
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("SYS_COMB_DATA")))
            {
                p.SetAttributeValue("SYS_COMB_DATA", "");
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("PRICE_FAMILY")))
            {
                p.SetAttributeValue("PRICE_FAMILY", "");
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("PRODUCT_STYLES")))
            {
                p.SetAttributeValue("PRODUCT_STYLES", "");
            }
            if (!string.IsNullOrEmpty(p.GetAttributeValue("VARIATION_SECTION")))
            {
                p.SetAttributeValue("VARIATION_SECTION", "");
            }

            return p;
        }

        private List<Product> ProductsFromCombinationsGet(Product mainProduct)
        {
            var mods = new List<Product>();
            if (mainProduct == null)
            {
                return mods;
            }

            foreach (var c in mainProduct.CombinationsGet())
            {
                var mod = mainProduct.Clone();
                mod.Category = mainProduct.Category;
                mod.FullDescription = c.FullDescription;
                mod.Name = c.Name;
                mod.Url = c.Url;
                //mod.SetAttributeValue("SYS_PRODUCT_WEIGHT", c.GetAttributeValue("SYS_PRODUCT_WEIGHT"));
                mod.CombinationsRemove();
                mod.DynamicAttributesClear();
                foreach (var attr in c.GetDynamicAttributes())
                {
                    //if (attr.Key.Contains("Вес упаковки"))
                    //{
                    //    mod.SetAttributeValue("SYS_PRODUCT_WEIGHT", c.GetAttributeValue(attr.Value));
                    //}
                    mod.AddDynamicAttribute(attr.Key, c.GetAttributeValue(attr.Value));
                }
                mod.SetAttributeValue("SYS_PRODUCT_WEIGHT", c.Weight);
                mod.SetAttributeValue("SYS_PRODUCT_SKU", c.Art);
                mod.SetAttributeValue("SYS_PRODUCT_ART", c.Art);
                mod.Quantity = c.Quantity;
                mod.Price = c.Price;
                mod.PriceOld = c.PriceOld;
                mod.ImagesClear();
                foreach (var img in c.ImagesGet())
                {
                    mod.ImageAdd(img.Key, img.Value);
                }
                mods.Add(mod);
            }
            return mods;
        }

        private string IDCreate(string sourceID)
        {
            if (string.IsNullOrEmpty(sourceID))
            {
                return sourceID;
            }

            sourceID = Regex.Replace(sourceID, @"\D", "");
            var arr = sourceID.ToCharArray();
            var newChars = new List<char>();

            for (int i = 0; i < arr.Length; i++)
            {
                if (i > 0 && i % 3 == 0)
                {
                    newChars.Add('.');
                }
                newChars.Add(arr[i]);
            }

            string newID = new string(newChars.ToArray());

            if (!string.IsNullOrEmpty(newID))
            {
                newID = newID.TrimEnd('.');
            }

            return newID;
        }

        private bool IsDuplicateCategory(Combination cmb, List<int> propsHashes)
        {
            var attrs = cmb.GetDynamicAttributes();
            if (attrs.Count < 1)
            {
                return false;//return true;
            }

            var strBuilder = new StringBuilder();
            foreach (var attr in attrs)
            {
                strBuilder.Append(cmb.GetAttributeValue(attr.Value));
            }
            int hash = strBuilder.ToString()
                                 .GetHashCode();

            if (propsHashes.Contains(hash))
            {
                return true;
            }

            propsHashes.Add(hash);
            return false;
        }

        private HttpMethodParameters _hmpForQuantity = new HttpMethodParameters()
        {
            MethodName = "GET",
            Accept = "application/json;version=2",
            //Accept = "application/vnd.ikea.iows+json;version=2.0",
            httpHeaders = new Dictionary<string, string>()
            {
            /*
             * https://www.ikea.com/ru/ru/products/javascripts/range-pip-main.751a00ccf1e16b028440.js
             * "x-client-id" прописан хардкодом в скрипте.
             * В один прекрасный момент, все может измениться :)
             */
            { "X-Client-Id", "b6c117e5-ae61-4ef5-b4cc-e0b1e37f0631" },
            { "accept-encoding", "gzip, deflate, br" },

            /*
             * https://www.ikea.com/ext/us/task-rabbit/js/script.min.js
            {"Consumer", "MAMMUT"},
            {"Contract", "37249"},
             */
            }
        };

        private int ProductQuantityGet(string shopCode, string productArt)
        {
            int qty = -1;
            _mti.AddLogFormatted("[ProductQuantityGet] Started. Shop code:'{0}'.", shopCode);
            var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
            if (hpl.Load(string.Format("https://api.ingka.ikea.com/cia/availabilities/ru/us?itemNos={0}&expand=StoresList,Restocks", productArt), false, _hmpForQuantity))
            {
                try
                {
                    var availObject = JsonConvert.DeserializeObject<ModelsIkea.Availability.AvailabilityObject>(hpl.Content);
                    var shopAvailObj = availObject.availabilities.FirstOrDefault(av => av.classUnitKey.classUnitCode == shopCode);
                    if (shopAvailObj == null)
                    {
                        _mti.AddLogFormatted("[ProductQuantityGet] Can't find availability info for shop code:'{0}'.", shopCode);
                    }
                    else
                    {
                        qty = shopAvailObj.buyingOption.cashCarry.availability != null ? shopAvailObj.buyingOption.cashCarry.availability.quantity : 0;
                    }
                }
                catch (Exception ex)
                {
                    _mti.AddLogError("Availability parsing failed.", ex);
                    throw;
                }
            }
            _mti.AddLogFormatted("[ProductQuantityGet] Finished. Shop code:'{0}' -> Quantity:{1}.", shopCode, qty.ToString());
            return qty;
        }

        private AvailabilityInfo ProductAvailabilityInfoGet(string shopCode, string productArt, string html = null)
        {
            AvailabilityInfo avi = new AvailabilityInfo() { ItemNo = productArt };
            _mti.AddLogFormatted("[ProductAvailabilityInfoGet] Started.  Product Art:'{0}', Shop code:'{1}', qty html length:{2}.",
                productArt, shopCode, !string.IsNullOrEmpty(html) ? html.Length.ToString() : "0");

            if (string.IsNullOrEmpty(html))
            {
                var hpl = _gps.Proxy.GetHtmlPageLoaderEmpty();
                hpl.Load(string.Format(_ikeaSettings.RegionSettings.ProductAvailabilityUrlTemplate, productArt), true, _hmpForQuantity);
                html = hpl.Content;
            }
            //if (hpl.Load(string.Format("https://api.ingka.ikea.com/cia/availabilities/ru/us?itemNos={0}&expand=StoresList,Restocks,SalesLocations", productArt), false, _hmpForQuantity))
            try
            {
                var availObject = JsonConvert.DeserializeObject<ModelsIkea.Availability.AvailabilityObject>(html);
                ModelsIkea.Availability.Availability shopAvailObj = null;
                ModelsIkea.Availability.AvailabilityDataItem shopAvailData = null;
                if (availObject.availabilities != null)
                {
                    shopAvailObj = availObject.availabilities.FirstOrDefault(av => av.classUnitKey.classUnitCode == shopCode);
                }
                if (shopAvailObj == null)
                {
                    if (availObject.data != null)
                    {
                        shopAvailData = availObject.data.FirstOrDefault(d => d.classUnitKey.classUnitCode == shopCode && d.isInCashAndCarryRange.HasValue && d.isInCashAndCarryRange.Value);
                        if (shopAvailData != null)
                        {
                            avi.Qty = shopAvailData.availableStocks.First(s => s._type == "CASHCARRY").quantity;
                        }
                    }

                }
                else
                {
                    avi.Qty = shopAvailObj.buyingOption.cashCarry.availability != null ? shopAvailObj.buyingOption.cashCarry.availability.quantity : 0;
                }

                if (shopAvailObj == null && shopAvailData == null)
                {
                    _mti.AddLogFormatted("[ProductAvailabilityInfoGet] Can't find availability info for product art:'{0}' and shop code:'{1}'.", productArt, shopCode);
                }

                /*
                var salesLocationsObj = availObject.salesLocations.FirstOrDefault(sl => sl.classUnitKey.classUnitCode == shopCode);
                if (salesLocationsObj == null)
                {
                    _mti.AddLogFormatted("[ProductAvailabilityInfoGet] Can't find sales locations info for shop code:'{0}'.", shopCode);
                }
                else
                {
                    if (salesLocationsObj.childItems != null)
                    {
                        var salesLocations = salesLocationsObj.childItems
                            .Where(ch => ch.salesLocations != null
                            && ch.salesLocations.Length > 0);

                        if (salesLocations.Any())
                        {
                            foreach (var ch in salesLocations)
                            {
                                if (ch.salesLocations[0].aisleAndBin != null)
                                {
                                    avi.SalesLocations.Add(new SalesLocationInfo()
                                    {
                                        ItemNo = IDCreate(ch.itemKey.itemNo),
                                        Aisle = ch.salesLocations[0].aisleAndBin.aisle,
                                        Bin = ch.salesLocations[0].aisleAndBin.bin
                                    });
                                }
                            }
                        }
                    }
                    else
                    {
                        avi.SalesLocations = salesLocationsObj.salesLocations
                            .Where(s => s != null && s.aisleAndBin != null)
                            .Select(s => new SalesLocationInfo()
                            {
                                ItemNo = IDCreate(salesLocationsObj.itemKey.itemNo),
                                Aisle = s.aisleAndBin.aisle,
                                Bin = s.aisleAndBin.bin
                            }).ToList();
                    }
                }
                */
            }
            catch (Exception ex)
            {
                _mti.AddLogError("Availability parsing failed.", ex);
                throw;
            }
            _mti.AddLogFormatted("[ProductAvailabilityInfoGet] Finished. Product Art:'{0}', Shop code:'{1}' -> Quantity:{2}, SalesLocations count:{3}.",
                productArt, shopCode, avi.Qty.ToString(), avi.SalesLocations.Count.ToString());

            return avi;
        }
        private void ProductImagesRename(Product p)
        {
            var imgs = p.ImagesGet();
            p.ImagesClear();
            int idx = 1;
            var newImgs = new Dictionary<string, string>();

            foreach (var img in imgs)
            {
                newImgs[img.Key] = MakeImgName2Sku(img.Key, idx, p.Art.Replace(".", ""));//product.ID);
                idx++;
            }

            p.ImageAdd(newImgs);
        }

        private string MakeImgName2Sku(string imgHref, int idx, string source)
        {
            var expanImg = new Regex(@"\.(jpg|png|bmp|gif|jpeg)", RegexOptions.IgnoreCase).Match(imgHref).Value;
            if (string.IsNullOrEmpty(expanImg))
            {
                expanImg = ".jpg";
            }
            if (idx > 1)
            {
                return source + "-" + idx.ToString(CultureInfo.InvariantCulture) + expanImg;
            }
            return source + expanImg;
        }

        private Dictionary<string, double> PackageSizesGet(string html)
        {
            string pattern = @"var jProductData = (\{[\w\W]*?\});";

            if (string.IsNullOrEmpty(html) || !Regex.IsMatch(html, pattern) || string.IsNullOrEmpty(_product.Url))
            {
                return new Dictionary<string, double>();
            }

            string json = Regex.Match(html, pattern).Groups[1].Value;
            var sizeName2Value = new Dictionary<string, double>();

            try
            {
                string partNumber = new Uri(_product.Url).Segments.Last().TrimEnd(new[] { '/' });
                var jObj = JObject.Parse(json);
                var mainProdJtoken = jObj["product"]["items"].FirstOrDefault(j => ((string)j["partNumber"]).Equals(partNumber));

                if (mainProdJtoken == null)
                {
                    return new Dictionary<string, double>();
                }

                JArray pkgInfoArr = mainProdJtoken["pkgInfoArr"] as JArray;

                if (pkgInfoArr == null)
                {
                    return new Dictionary<string, double>();
                }

                double maxWidth = 0;
                double maxLength = 0;
                double packagesQty = 0;
                foreach (var pkgInfo in pkgInfoArr.SelectTokens("$..pkgInfo"))
                {
                    if (!_mti.CanContinue())
                    {
                        break;
                    }
                    foreach (var item in pkgInfo.Children())
                    {
                        var qtyProp = item["quantity"];
                        if (qtyProp == null)
                        {
                            continue;
                        }

                        int qty = (int)qtyProp;
                        packagesQty += qty;
                        foreach (var prop in item.Children<JProperty>())
                        {
                            string valueStr = (string)prop.Value;

                            if (string.IsNullOrEmpty(valueStr))
                            {
                                continue;
                            }

                            valueStr = Regex.Replace(valueStr, @"(?<=\d)\s.*", "")
                                            .Replace(".", ",");
                            double value;

                            if (!double.TryParse(valueStr, out value))
                            {
                                continue;
                            }

                            string name = prop.Name.Trim();
                            if (name.Equals("widthMet", StringComparison.InvariantCultureIgnoreCase))
                            {
                                maxWidth = (maxWidth > value) ? maxWidth : value;
                                continue;
                            }

                            if (name.Equals("lengthMet", StringComparison.InvariantCultureIgnoreCase))
                            {
                                maxLength = (maxLength > value) ? maxLength : value;
                                continue;
                            }

                            if (sizeName2Value.Keys.Contains(prop.Name.Trim()))
                            {
                                sizeName2Value[prop.Name.Trim()] += value * qty;
                            }
                            else
                            {
                                sizeName2Value[prop.Name.Trim()] = value;
                            }
                        }
                    }
                }
                sizeName2Value["widthMet"] = maxWidth;
                sizeName2Value["lengthMet"] = maxLength;
                sizeName2Value["packagesQty"] = packagesQty;

                for (int i = 0; i < sizeName2Value.Count; i++)
                {
                    sizeName2Value[sizeName2Value.ElementAt(i).Key] = Math.Round(sizeName2Value.ElementAt(i).Value, 2);
                }
            }
            catch (Exception ex)
            {
                _mti.AddLogError("<<<<< PackageSizesGet() failed.", ex);
            }

            return sizeName2Value;
        }

        private Dictionary<string, double> PackageSizesGet(JObject jObj, string partNumber)
        {
            if (string.IsNullOrEmpty(partNumber) || jObj == null)
            {
                return new Dictionary<string, double>();
            }

            var sizeName2Value = new Dictionary<string, double>();

            try
            {
                var mainProdJtoken = jObj["product"]["items"].FirstOrDefault(j => ((string)j["partNumber"]).Equals(partNumber));

                if (mainProdJtoken == null)
                {
                    return new Dictionary<string, double>();
                }

                JArray pkgInfoArr = mainProdJtoken["pkgInfoArr"] as JArray;

                if (pkgInfoArr == null)
                {
                    return new Dictionary<string, double>();
                }

                double maxWidth = 0;
                double maxLength = 0;
                double packagesQty = 0;
                foreach (var pkgInfo in pkgInfoArr.SelectTokens("$..pkgInfo"))
                {
                    if (!_mti.CanContinue())
                    {
                        break;
                    }
                    foreach (var item in pkgInfo.Children())
                    {
                        var qtyProp = item["quantity"];
                        if (qtyProp == null)
                        {
                            continue;
                        }

                        int qty = (int)qtyProp;
                        packagesQty += qty;
                        foreach (var prop in item.Children<JProperty>())
                        {
                            string valueStr = (string)prop.Value;

                            if (string.IsNullOrEmpty(valueStr))
                            {
                                continue;
                            }

                            valueStr = Regex.Replace(valueStr, @"(?<=\d)\s.*", "")
                                            .Replace(".", ",");
                            double value;

                            if (!double.TryParse(valueStr, out value))
                            {
                                continue;
                            }

                            string name = prop.Name.Trim();
                            if (name.Equals("widthMet", StringComparison.InvariantCultureIgnoreCase))
                            {
                                maxWidth = (maxWidth > value) ? maxWidth : value;
                                continue;
                            }

                            if (name.Equals("lengthMet", StringComparison.InvariantCultureIgnoreCase))
                            {
                                maxLength = (maxLength > value) ? maxLength : value;
                                continue;
                            }

                            if (sizeName2Value.Keys.Contains(prop.Name.Trim()))
                            {
                                sizeName2Value[prop.Name.Trim()] += value * qty;
                            }
                            else
                            {
                                sizeName2Value[prop.Name.Trim()] = value;
                            }
                        }
                    }
                }
                sizeName2Value["widthMet"] = maxWidth;
                sizeName2Value["lengthMet"] = maxLength;
                sizeName2Value["packagesQty"] = packagesQty;

                for (int i = 0; i < sizeName2Value.Count; i++)
                {
                    sizeName2Value[sizeName2Value.ElementAt(i).Key] = Math.Round(sizeName2Value.ElementAt(i).Value, 2);
                }
            }
            catch (Exception ex)
            {
                _mti.AddLogError("<<<<< PackageSizesGet() failed.", ex);
            }

            return sizeName2Value;
        }
        private void Init(GrabCatalogBuildScriptParameters p)
        {
            _mti = p.Process.m_ti;
            _category = new Category { ID = "0" };
        }
    }

    namespace ModelsIkea
    {
        namespace Info
        {
            public class InformationSection
            {
                public bool isClient { get; set; }
                public Dimensionprops dimensionProps { get; set; }
                public Productdetailsprops productDetailsProps { get; set; }
                public object productReviewsProps { get; set; }
                public bool technicalInformationProps { get; set; }
            }

            public class Dimensionprops
            {
                public string title { get; set; }
                public string subtitle { get; set; }
                public Dimension[] dimensions { get; set; }
                public object[] images { get; set; }
                public Fallbackimage fallbackImage { get; set; }
                public string eventLabel { get; set; }
            }

            public class Fallbackimage
            {
                public string alt { get; set; }
                public string id { get; set; }
                public string imageFileName { get; set; }
                public string url { get; set; }
                public string type { get; set; }
            }

            public class Dimension
            {
                public string type { get; set; }
                public string name { get; set; }
                public string measure { get; set; }
            }

            public class Productdetailsprops
            {
                public string title { get; set; }
                public string eventLabel { get; set; }
                public Productdescriptionprops productDescriptionProps { get; set; }
                public Accordionobject accordionObject { get; set; }
            }

            public class Productdescriptionprops
            {
                public string[] paragraphs { get; set; }
                public string designerLabel { get; set; }
                public string designerName { get; set; }
            }

            public class Accordionobject
            {
                public Materialsandcare materialsAndCare { get; set; }
                public object sustainabilityAndEnvironment { get; set; }
                public Packaging packaging { get; set; }
                public Assemblyanddocuments assemblyAndDocuments { get; set; }
            }

            public class Materialsandcare
            {
                public string id { get; set; }
                public string title { get; set; }
                public Contentprops contentProps { get; set; }
            }

            public class Contentprops
            {
                public Material[] materials { get; set; }
                public Careinstruction[] careInstructions { get; set; }
                public object[] proposition65 { get; set; }
                public Package[] packages { get; set; }
                public string totalNoOfPackagesText { get; set; }
                public Attachments attachments { get; set; }
            }

            public class Material
            {
                public string productType { get; set; }
                //public Material1[] materials { get; set; }
                public Material[] materials { get; set; }
                public string material { get; set; }
                public string part { get; set; }
            }

            //public class Material1
            //{
            //    public string material { get; set; }
            //    public string part { get; set; }
            //}

            public class Careinstruction
            {
                public string[] texts { get; set; }
            }

            public class Packaging
            {
                public string id { get; set; }
                public string title { get; set; }
                //public Contentprops1 contentProps { get; set; }
                public Contentprops contentProps { get; set; }

            }

            //public class Contentprops1
            //{
            //    public Package[] packages { get; set; }
            //    public string totalNoOfPackagesText { get; set; }
            //}

            public class Package
            {
                public string name { get; set; }
                public string typeName { get; set; }
                public Articlenumber articleNumber { get; set; }
                public Measurement[][] measurements { get; set; }
                public Quantity quantity { get; set; }
                public bool multiPackDisclaimerText { get; set; }
            }

            public class Articlenumber
            {
                public string label { get; set; }
                public string value { get; set; }
            }

            public class Quantity
            {
                public string label { get; set; }
                public int value { get; set; }
            }

            public class Measurement
            {
                public string label { get; set; }
                public string value { get; set; }
            }

            public class Assemblyanddocuments
            {
                public string id { get; set; }
                public string title { get; set; }
                //public Contentprops2 contentProps { get; set; }
                public Contentprops contentProps { get; set; }
            }

            //public class Contentprops2
            //{
            //    public Attachments attachments { get; set; }
            //}

            public class Attachments
            {
                public Assembly assembly { get; set; }
                public Software software { get; set; }
                public Other other { get; set; }
                public Disassembly disassembly { get; set; }
            }

            public class Assembly
            {
                public string header { get; set; }
                public Attachment[] attachments { get; set; }
            }

            public class Attachment
            {
                public string id { get; set; }
                public string url { get; set; }
                public string label { get; set; }
            }

            public class Software
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }

            public class Other
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }

            public class Disassembly
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }
        }

        namespace Price
        {

            public class PricePackage
            {
                public object newProductText { get; set; }
                public string newLowerPriceText { get; set; }
                public object familyText { get; set; }
                public object timeRestrictedOfferText { get; set; }
                public string productName { get; set; }
                public string productDescription { get; set; }
                public string measurementText { get; set; }
                public Price price { get; set; }
                public object productReviewsProps { get; set; }
                public object productDiscountsProps { get; set; }
                public object ecoWeeeFees { get; set; }
                public object timeFrame { get; set; }
                public object vatInfoText { get; set; }
                public object energyLabelProps { get; set; }
                public string ecoWeeeFeeExcludeText { get; set; }
                public string totalPriceText { get; set; }
                public string ecoWeeeFeeLinkText { get; set; }
                public object ecoWeeeFeeLink { get; set; }
                public object validToFromText { get; set; }
                public object validToFromAndWhileSupplyLastText { get; set; }
                public object whileSupplyLastText { get; set; }
            }

            public class Price
            {
                public Mainpriceprops mainPriceProps { get; set; }
                public Previouspriceprops previousPriceProps { get; set; }
                public string previousPriceText { get; set; }
                public object comparisonPriceText { get; set; }
                public string discount { get; set; }
            }

            public class Mainpriceprops
            {
                public Price1 price { get; set; }
                public string unit { get; set; }
                public string currencySymbol { get; set; }
                public string separator { get; set; }
                public bool hasTrailingCurrency { get; set; }
                public bool hasHighlight { get; set; }
                public bool hasStrikeThrough { get; set; }
                public bool isSmall { get; set; }
                public bool isSecondary { get; set; }
            }

            public class Price1
            {
                public string integer { get; set; }
                public string decimals { get; set; }
            }

            public class Previouspriceprops
            {
                //public Price2 price { get; set; }
                public Price1 price { get; set; }
                public string unit { get; set; }
                public string currencySymbol { get; set; }
                public string separator { get; set; }
                public bool hasTrailingCurrency { get; set; }
                public bool hasHighlight { get; set; }
                public bool hasStrikeThrough { get; set; }
                public bool isSmall { get; set; }
                public bool isSecondary { get; set; }
            }

            //public class Price2
            //{
            //    public string integer { get; set; }
            //    public string decimals { get; set; }
            //}

        }

        namespace Variations
        {

            public class VariationsSection
            {
                public List<Variation> variations { get; set; }
                public bool disabled { get; set; }
                public VariationsSection()
                {
                    variations = new List<Variation>();
                }
            }

            public class Variation
            {
                public string title { get; set; }
                public string selectedOption { get; set; }
                public List<Option> allOptions { get; set; } // akl_02112021: a new field of variations appeared.
                public List<Option> options { get; set; }
                public string code { get; set; }
                public Variation()
                {
                    options = new List<Option>();
                    allOptions = new List<Option>();
                }
            }

            public class Option
            {
                public string url { get; set; }
                public string title { get; set; }
                public bool isSelected { get; set; }
                public string linkId { get; set; }
                public Image image { get; set; }
                public Priceprops priceProps { get; set; }
            }

            public class Image
            {
                public string alt { get; set; }
                public string id { get; set; }
                public string imageFileName { get; set; }
                public string url { get; set; }
                public string type { get; set; }
            }

            public class Priceprops
            {
                public string priceNumeral { get; set; }
                public Revampprice revampPrice { get; set; }
                public Priceformat priceFormat { get; set; }
            }

            public class Revampprice
            {
                public int numDecimals { get; set; }
                public string separator { get; set; }
                public string integer { get; set; }
                public string decimals { get; set; }
                public string currencySymbol { get; set; }
                public string currencyPrefix { get; set; }
                public string currencySuffix { get; set; }
                public bool hasTrailingCurrency { get; set; }

                public double PriceDoubleGet()
                {
                    var fmt = new NumberFormatInfo();
                    fmt.NumberDecimalSeparator = separator;
                    double r;
                    if (double.TryParse(string.Format("{0}{1}{2}", integer, separator, decimals), NumberStyles.Any, fmt, out r))
                    {
                        return r;
                    }
                    return 0;
                }
            }

            public class Priceformat
            {
                public string currencyPrefix { get; set; }
                public string currencySuffix { get; set; }
                public string priceFormatter { get; set; }
            }

        }

        //namespace Availability
        //{

        //    public class AvailabilityResponse
        //    {
        //        public Stockavailability StockAvailability { get; set; }
        //    }

        //    public class Stockavailability
        //    {
        //        public Classunitkey ClassUnitKey { get; set; }
        //        public Itemkey ItemKey { get; set; }
        //        public Retailitemavailability RetailItemAvailability { get; set; }
        //        public Availablestockforecastlist AvailableStockForecastList { get; set; }
        //        public Xmlns xmlns { get; set; }
        //    }

        //    public class Classunitkey
        //    {
        //        public Classtype ClassType { get; set; }
        //        public Classunittype ClassUnitType { get; set; }
        //        public Classunitcode ClassUnitCode { get; set; }
        //    }

        //    public class Classtype
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }


        //    public class Classunittype
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Classunitcode
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Itemkey
        //    {
        //        public Itemno ItemNo { get; set; }
        //        public Itemtype ItemType { get; set; }
        //    }

        //    public class Itemno
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Itemtype
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Retailitemavailability
        //    {
        //        public Availablestock AvailableStock { get; set; }
        //        public Availablestocktype AvailableStockType { get; set; }
        //        public Instockprobabilitycode InStockProbabilityCode { get; set; }
        //        public Instockrangecode InStockRangeCode { get; set; }
        //        public Incustomerorderrangecode InCustomerOrderRangeCode { get; set; }
        //        public Retailitemcommchildavailabilitylist RetailItemCommChildAvailabilityList { get; set; }
        //    }

        //    public class Availablestock
        //    {
        //        [JsonProperty("$")]
        //        public int? ItemVal { get; set; }
        //    }

        //    public class Availablestocktype
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Instockprobabilitycode
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Instockrangecode
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Incustomerorderrangecode
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Retailitemcommchildavailabilitylist
        //    {
        //        public Retailitemcommchildavailability[] RetailItemCommChildAvailability { get; set; }
        //    }

        //    public class Retailitemcommchildavailability
        //    {
        //        //public Itemno1 ItemNo { get; set; }
        //        public Itemno ItemNo { get; set; }
        //        //public Itemtype1 ItemType { get; set; }
        //        public Itemtype ItemType { get; set; }
        //        //public Availablestocktype1 AvailableStockType { get; set; }
        //        public Availablestocktype AvailableStockType { get; set; }
        //        public Recommendedsaleslocation RecommendedSalesLocation { get; set; }
        //        public Salesmethodcode SalesMethodCode { get; set; }
        //    }

        //    //public class Itemno1
        //    //{
        //    //    public object _ { get; set; }
        //    //}

        //    //public class Itemtype1
        //    //{
        //    //    public string _ { get; set; }
        //    //}

        //    //public class Availablestocktype1
        //    //{
        //    //    public string _ { get; set; }
        //    //}

        //    public class Recommendedsaleslocation
        //    {
        //    }

        //    public class Salesmethodcode
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Availablestockforecastlist
        //    {
        //        public Availablestockforecast[] AvailableStockForecast { get; set; }
        //    }

        //    public class Availablestockforecast
        //    {
        //        //public Availablestock1 AvailableStock { get; set; }
        //        public Availablestock AvailableStock { get; set; }
        //        //public Availablestocktype2 AvailableStockType { get; set; }
        //        public Availablestocktype AvailableStockType { get; set; }
        //        //public Instockprobabilitycode1 InStockProbabilityCode { get; set; }
        //        public Instockprobabilitycode InStockProbabilityCode { get; set; }
        //        public Validdatetime ValidDateTime { get; set; }
        //        public Validdatetimeunit ValidDateTimeUnit { get; set; }
        //    }

        //    //public class Availablestock1
        //    //{
        //    //    public int _ { get; set; }
        //    //}

        //    //public class Availablestocktype2
        //    //{
        //    //    public string _ { get; set; }
        //    //}

        //    //public class Instockprobabilitycode1
        //    //{
        //    //    public string _ { get; set; }
        //    //}

        //    public class Validdatetime
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Validdatetimeunit
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //    public class Xmlns
        //    {
        //        [JsonProperty("$")]
        //        public string ItemVal { get; set; }
        //    }

        //}

        namespace Availability
        {
            public class AvailabilityObject
            {
                public Availability[] availabilities { get; set; }
                public SalesLocation[] salesLocations { get; set; }
                public AvailabilityDataItem[] data { get; set; }
                public DateTime timestamp { get; set; }
                public string traceId { get; set; }
            }

            public class AvailabilityDataItem
            {
                public bool? isInCashAndCarryRange { get; set; }
                public bool isInHomeDeliveryRange { get; set; }
                public AvailableStock[] availableStocks { get; set; }
                public Classunitkey classUnitKey { get; set; }
                public Itemkey itemKey { get; set; }
            }

            public class AvailableStock
            {
                [JsonProperty("type")]
                public string _type { get; set; }
                public int quantity { get; set; }
            }

            public class SalesLocation
            {
                public Childitem[] childItems { get; set; }
                public Classunitkey classUnitKey { get; set; }
                public Itemkey itemKey { get; set; }
                public Saleslocation[] salesLocations { get; set; }
            }

            //public class Classunitkey
            //{
            //    public string classUnitCode { get; set; }
            //    public string classUnitType { get; set; }
            //}

            //public class Itemkey
            //{
            //    public string itemNo { get; set; }
            //    public string itemType { get; set; }
            //}

            public class Childitem
            {
                public Itemkey itemKey { get; set; }
                public Saleslocation[] salesLocations { get; set; }
            }

            //public class Itemkey1
            //{
            //    public string itemNo { get; set; }
            //    public string itemType { get; set; }
            //}

            public class Saleslocation
            {
                public Aisleandbin aisleAndBin { get; set; }
                public string division { get; set; }
                public string id { get; set; }
                public int recommendationRank { get; set; }
                public string type { get; set; }
            }

            public class Aisleandbin
            {
                public string aisle { get; set; }
                public string bin { get; set; }
            }


            public class Availability
            {
                public bool availableForCashCarry { get; set; }
                public bool availableForClickCollect { get; set; }
                public bool availableForHomeDelivery { get; set; }
                public Buyingoption buyingOption { get; set; }
                public Classunitkey classUnitKey { get; set; }
                public Itemkey itemKey { get; set; }
            }

            public class Buyingoption
            {
                public Cashcarry cashCarry { get; set; }
                public Homedelivery homeDelivery { get; set; }
            }

            public class Cashcarry
            {
                public Availability1 availability { get; set; }
                public Range range { get; set; }
                public string unitOfMeasure { get; set; }
            }

            public class Availability1
            {
                public Probability probability { get; set; }
                public int quantity { get; set; }
                public DateTime updateDateTime { get; set; }
                public Restock[] restocks { get; set; }
            }

            public class Probability
            {
                public Thisday thisDay { get; set; }
                public DateTime updateDateTime { get; set; }
            }

            public class Thisday
            {
                public Colour colour { get; set; }
                public string messageType { get; set; }
            }

            public class Colour
            {
                public string rgbDec { get; set; }
                public string rgbHex { get; set; }
            }

            public class Restock
            {
                public string earliestDate { get; set; }
                public string latestDate { get; set; }
                public int quantity { get; set; }
                public string reliability { get; set; }
                public string type { get; set; }
                public DateTime updateDateTime { get; set; }
            }

            public class Range
            {
                public bool inRange { get; set; }
            }

            public class Homedelivery
            {
                public Range1 range { get; set; }
                public Availability2 availability { get; set; }
            }

            public class Range1
            {
                public bool inRange { get; set; }
            }

            public class Availability2
            {
                public Probability1 probability { get; set; }
                public DateTime updateDateTime { get; set; }
            }

            public class Probability1
            {
                public Thisday1 thisDay { get; set; }
                public DateTime updateDateTime { get; set; }
            }

            public class Thisday1
            {
                public Colour1 colour { get; set; }
                public string messageType { get; set; }
            }

            public class Colour1
            {
                public string rgbDec { get; set; }
                public string rgbHex { get; set; }
            }

            public class Classunitkey
            {
                public string classUnitCode { get; set; }
                public string classUnitType { get; set; }
            }

            public class Itemkey
            {
                public string itemNo { get; set; }
                public string itemType { get; set; }
            }


        }

        namespace Cataloge
        {

            public class Category
            {
                public string id { get; set; }
                public int depth { get; set; }
                public List<Subcategory> subcategories { get; set; }
                public Category()
                {
                    subcategories = new List<Subcategory>();
                }
            }

            public class Subcategory
            {
                public string id { get; set; }
                public string link { get; set; }
                public string title { get; set; }
                public bool isCrossReferenced { get; set; }
            }

        }

        namespace Media
        {
            public class MediaListBase
            {
                public string type { get; set; }
                public Content content { get; set; }
            }
            public class MediaGrid
            {
                public List<Fullmedialist> fullMediaList { get; set; }
                public List<Medialist> mediaList { get; set; }
                public int heroesCount { get; set; }
                public bool isDoubleHero { get; set; }
                public bool isSingleHero { get; set; }
                public bool disabledButton { get; set; }
                public string buttonMoreLabel { get; set; }
                public string buttonLessLabel { get; set; }
                public int barPosition { get; set; }
                public float barWidth { get; set; }
                public bool isSingleMedia { get; set; }
                public object energyLabelProps { get; set; }
                public MediaGrid()
                {
                    fullMediaList = new List<Fullmedialist>();
                    mediaList = new List<Medialist>();
                }
            }

            public class Fullmedialist : MediaListBase
            {
                //public string type { get; set; }
                //public Content content { get; set; }
            }

            public class Content
            {
                public string alt { get; set; }
                public string id { get; set; }
                public string imageFileName { get; set; }
                public string url { get; set; }
                public string type { get; set; }
            }

            public class Medialist : MediaListBase
            {
                //public string type { get; set; }
                //public Content content { get; set; }
            }
        }

        namespace Pagination
        {

            public class DataCategory
            {
                public string id { get; set; }
                public Pagination pagination { get; set; }
                public int totalCount { get; set; }
            }

            public class Pagination
            {
                public int currentPage { get; set; }
                public Next next { get; set; }
                public object prev { get; set; }
            }

            public class Next
            {
                public string url { get; set; }
                public string label { get; set; }
            }


            //public class MoreproductsResponse
            //{
            //    public string usergroup { get; set; }
            //    public Moreproducts moreProducts { get; set; }
            //}

            //public class Moreproducts
            //{
            //    public List<Productwindow> productWindow { get; set; }
            //    public Moreproducts()
            //    {
            //        productWindow = new List<Productwindow>();
            //    }
            //}

            //public class Productwindow
            //{
            //    public string name { get; set; }
            //    public string typeName { get; set; }
            //    public string itemMeasureReferenceText { get; set; }
            //    public string mainImageUrl { get; set; }
            //    public string pipUrl { get; set; }
            //    public string id { get; set; }
            //    public string metadata { get; set; }
            //    public bool onlineSellable { get; set; }
            //    public Gprdescription gprDescription { get; set; }
            //    public Color[] colors { get; set; }
            //    public float priceNumeral { get; set; }
            //    public string currencyCode { get; set; }
            //}

            //public class Gprdescription
            //{
            //    public object[] colors { get; set; }
            //    public int numberOfSizes { get; set; }
            //    public int numberOfColors { get; set; }
            //}

            //public class Color
            //{
            //    public string name { get; set; }
            //    public string id { get; set; }
            //}

            public class MoreproductsResponse
            {
                public string usergroup { get; set; }
                public Moreproducts moreProducts { get; set; }
            }

            public class Moreproducts
            {
                public List<Productwindow> productWindow { get; set; }
                public Moreproducts()
                {
                    productWindow = new List<Productwindow>();
                }
            }

            public class Productwindow
            {
                public string name { get; set; }
                public string typeName { get; set; }
                public string itemMeasureReferenceText { get; set; }
                public string mainImageUrl { get; set; }
                public string pipUrl { get; set; }
                public string id { get; set; }
                public string itemNoGlobal { get; set; }
                public string metadata { get; set; }
                public bool onlineSellable { get; set; }
                public bool lastChance { get; set; }
                public Gprdescription gprDescription { get; set; }
                public Color[] colors { get; set; }
                public double priceNumeral { get; set; }
                public string currencyCode { get; set; }
            }

            public class Gprdescription
            {
                public int numberOfVariants { get; set; }
                public List<Variant> variants { get; set; }
                public Gprdescription()
                {
                    variants = new List<Variant>();
                }
            }

            public class Variant
            {
                public string id { get; set; }
                public string pipUrl { get; set; }
                public string imageUrl { get; set; }
                public string imageAlt { get; set; }
                //public Price price { get; set; }
            }
            public class Price
            {
                public string prefix { get; set; }
                public string wholeNumber { get; set; }
                public string separator { get; set; }
                public string decimals { get; set; }
                public string suffix { get; set; }
                public bool isRegularCurrency { get; set; }
            }

            public class Color
            {
                public string name { get; set; }
                public string id { get; set; }
                public string hex { get; set; }
            }
        }
        namespace ProductProps
        {
            /*
             * akl_02152021:
             * Теперь информация об упаковках приходит внутри html страницы товара.
             */

            public class GeneralDataProps
            {
                public string productName { get; set; }
                public bool hideSection { get; set; }
                public string productNumber { get; set; }
                public string productType { get; set; }
                public bool onlineSellable { get; set; }
                public bool enabledCartButton { get; set; }
                public string noJavascriptLabel { get; set; }
                public string currency { get; set; }
                public string productPrice { get; set; }
                public string ratings { get; set; }
            }

            public class Props
            {
                public bool isClient { get; set; }
                public JToken dimensionProps { get; set; }
                public Dimensionprops DimensionPropsObject
                {
                    get
                    {
                        Dimensionprops dp = null;
                        try
                        {
                            var jo = dimensionProps as JObject;
                            if (jo != null)
                            {
                                dp = jo.ToObject<Dimensionprops>();
                            }
                        }
                        catch { }
                        return dp;
                    }
                }
                public Productdetailsprops productDetailsProps { get; set; }
                // public object productReviewsProps { get; set; }
                //public bool technicalInformationProps { get; set; }
            }

            public class Dimensionprops
            {
                public string title { get; set; }
                public Packaging packaging { get; set; }
                //public Dimension[] dimensions { get; set; }
                //public object[] images { get; set; }
                //public Fallbackimage fallbackImage { get; set; }
                //public string eventLabel { get; set; }
            }

            public class Fallbackimage
            {
                public string alt { get; set; }
                public string id { get; set; }
                public string imageFileName { get; set; }
                public string url { get; set; }
                public string type { get; set; }
            }

            public class Dimension
            {
                public string type { get; set; }
                public string name { get; set; }
                public string measure { get; set; }
            }

            public class Productdetailsprops
            {
                public string title { get; set; }
                public string eventLabel { get; set; }
                public Productdescriptionprops productDescriptionProps { get; set; }
                public Accordionobject accordionObject { get; set; }
            }

            public class Productdescriptionprops
            {
                public string[] paragraphs { get; set; }
                public string designerLabel { get; set; }
                public string designerName { get; set; }
            }

            public class Accordionobject
            {
                public Materialsandcare materialsAndCare { get; set; }
                public object sustainabilityAndEnvironment { get; set; }
                public Packaging packaging { get; set; }
                public Assemblyanddocuments assemblyAndDocuments { get; set; }
            }

            public class Materialsandcare
            {
                public string id { get; set; }
                public string title { get; set; }
                public Contentprops contentProps { get; set; }
            }

            public class Contentprops
            {
                public Material[] materials { get; set; }
                public object[] careInstructions { get; set; }
                public object[] proposition65 { get; set; }
            }

            public class Material
            {
                public string productType { get; set; }
                public Material1[] materials { get; set; }
            }

            public class Material1
            {
                public string part { get; set; }
                public string material { get; set; }
            }

            public class Packaging
            {
                public string id { get; set; }
                public string title { get; set; }
                public Contentprops1 contentProps { get; set; }
            }

            public class Contentprops1
            {
                public List<Package> packages { get; set; }
                public string totalNoOfPackagesText { get; set; }
                public Contentprops1()
                {
                    packages = new List<Package>();
                }
            }

            public class Package
            {
                public string name { get; set; }
                public string typeName { get; set; }
                public Articlenumber articleNumber { get; set; }
                public Measurement[][] measurements { get; set; }
                public Quantity quantity { get; set; }
                public string multiPackDisclaimerText { get; set; }
            }

            public class Articlenumber
            {
                public string label { get; set; }
                public string value { get; set; }
            }

            public class Quantity
            {
                public string label { get; set; }
                public int value { get; set; }
            }

            public class Measurement
            {
                public string label { get; set; }
                public string value { get; set; }
            }

            public class Assemblyanddocuments
            {
                public string id { get; set; }
                public string title { get; set; }
                public Contentprops2 contentProps { get; set; }
            }

            public class Contentprops2
            {
                public Attachments attachments { get; set; }
            }

            public class Attachments
            {
                public Assembly assembly { get; set; }
                public Software software { get; set; }
                public Other other { get; set; }
                public Disassembly disassembly { get; set; }
            }

            public class Assembly
            {
                public string header { get; set; }
                public Attachment[] attachments { get; set; }
            }

            public class Attachment
            {
                public string id { get; set; }
                public string url { get; set; }
                public string label { get; set; }
            }

            public class Software
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }

            public class Other
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }

            public class Disassembly
            {
                public string header { get; set; }
                public object[] attachments { get; set; }
            }

        }
    }
}