<?php

namespace KupShop\CatalogBundle\Util;

use Doctrine\DBAL\Connection;
use KupShop\CatalogBundle\Entity\Wrapper\ProductWrapper;
use KupShop\CatalogBundle\Query\ProductsTypes;
use KupShop\ContentBundle\Entity\Wrapper\ProductUnifiedWrapper;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\StoresBundle\Utils\StoresInStore;
use Query\Operator;
use Query\Operator as Op;
use Query\QueryBuilder;

class InsertProductsTypes
{
    public function call($type, &$params, $productList, $filterParams)
    {
        $method = preg_replace_callback('/(?:^|_)(.?)/', function ($matches) {
            return strtoupper($matches[1]);
        }, $type);

        $method = 'handle'.$method;
        if (!method_exists($this, $method)) {
            throw new \InvalidArgumentException(sprintf('There is no method \'%s::%s()\' for product type \'%s\'', get_class($this), $method, $type));
        }

        return call_user_func_array([$this, $method], [&$params, $productList, $filterParams]);
    }

    public function handleSpec(&$params)
    {
        return $params['spec'] ?? null;
    }

    public function handleProductsFilter(&$params)
    {
        $productsFilterSpec = ServiceContainer::getService(ProductsFilterSpecs::class);

        return $productsFilterSpec->getSpecs($params['filter'] ?? []);
    }

    public function handleMultiSets(&$params)
    {
        return function (QueryBuilder $qb) use ($params) {
            $qb->join('p', 'set_products', 'sp', 'sp.id_product_master = p.id AND sp.id_product=:slave')
                ->setParameter('slave', $params['id']);
        };
    }

    public function handleUserMostBought()
    {
        $user = \User::getCurrentUser();

        return function (QueryBuilder $qb) use ($user) {
            $qb->join('p', 'order_items', 'oi', 'oi.id_product = p.id')
                ->join('oi', 'orders', 'o', 'o.id = oi.id_order')
                ->andWhere(Op::equals(['o.id_user' => $user->id ?? null]))
                ->orderBy('COUNT(p.id)', 'DESC');
        };
    }

    public function handleTopSoldPriceProduct(&$params)
    {
        $params['order'] = 'p.pieces_sold * p.price DESC';
    }

    public function handleRandomDiscountedProduct(&$params)
    {
        if (!isset($params['random'])) {
            $params['random'] = true;
        }

        return function (QueryBuilder $qb) use ($params) {
            if (empty($params['discount'])) {
                $params['discount'] = 0;
            }

            $qb->andWhere('p.discount > :discount_value')
                ->setParameter('discount_value', $params['discount']);
        };
    }

    public function handleNewestProduct(&$params)
    {
        if (!isset($params['order'])) {
            $params['order'] = 'p.date_added DESC';
        }
    }

    public function handleProduct(&$params)
    {
        if (!is_array($params['id'])) {
            $params['id'] = [$params['id']];
        }

        return Operator::inIntArray($params['id'], 'p.id');
    }

    public function handleCampaignProduct(&$params)
    {
        if (!isset($params['order'])) {
            $params['order'] = 'p.updated DESC';
        }

        return \Query\Filter::byCampaigns([$params['campaign']]);
    }

    public function handleLabelProduct(&$params)
    {
        if (!is_array($params['labelId'])) {
            $params['labelId'] = (array) $params['labelId'];
        }

        return \Query\Filter::byLabels($params['labelId'], true);
    }

    public function handleTopProduct(&$params)
    {
        $params['order'] = 'FIND_IN_SET(\'S\', p.campaign) DESC, p.pieces_sold DESC';
    }

    public function handleRelatedProduct(&$params)
    {
        if (empty($params['products'])) {
            if (!empty($params['product'])) {
                $params['products'] = [$params['product']];
            } else {
                if (isDevelopment()) {
                    throw new \InvalidArgumentException('Chybí parametr "products"');
                }
                $params['products'] = [];
            }
        }

        $params['products'] = array_map(function ($x) {
            return isset($x['id']) ? $x['id'] : $x;
        }, $params['products']);

        if (empty($params['order'])) {
            $params['order'] = 'rel.position ASC';
        }

        return function (QueryBuilder $qb) use ($params) {
            $qb->leftJoin('p', 'products_related', 'rel', 'rel.id_rel_product=p.id');

            $ret = Op::inIntArray($params['products'], 'rel.id_top_product');
            if ($params['related_type'] ?? false) {
                $ret = Op::andX(
                    $ret,
                    Op::equals(['rel.type' => $params['related_type']]),
                );
            }

            if (!empty($params['symmetric'])) {
                $qb->leftJoin('p', 'products_related', 'rel2', 'rel2.id_top_product=p.id');
                $ret = Op::orX($ret,
                    Op::andX(
                        Op::inIntArray($params['products'], 'rel2.id_rel_product'),
                        isset($params['related_type']) ? Op::equals(['rel2.type' => $params['related_type']]) : null
                    )
                );
            }

            return $ret;
        };
    }

    public function handleCollectionProduct(&$params)
    {
        if (empty($params['products'])) {
            if (empty($params['product'])) {
                throw new \InvalidArgumentException('Chybí parametr "product" nebo "products"');
            }

            $params['products'] = [$params['product']];
        }

        $ids = array_map(function ($x) {
            if (isset($x['id'])) {
                return (int) $x['id'];
            }

            return (int) $x;
        }, $params['products']);

        return function (QueryBuilder $qb) use ($ids) {
            $collectionProducts = \Query\Product::inCollectionsProducts($qb, $ids);
            $qb->join('p', "({$collectionProducts})", 'c', 'c.id = p.id');
        };
    }

    public function handleIndexProduct(&$params)
    {
        if (!isset($params['random'])) {
            $params['random'] = true;
        }

        if (!isset($params['order'])) {
            $params['order'] = 'p.date_added DESC';
        }

        return Op::findInSet(['L'], 'p.campaign');
    }

    public function handleCartProduct(&$params)
    {
        if (!isset($params['products'])) {
            if (!isset($params['product'])) {
                throw new \InvalidArgumentException('Chybí parametr "products"');
            }

            $params['products'] = [$params['product']];
        }

        $id_products = array_map(function ($x) {
            return $x['id'] ?? $x;
        }, $params['products']);

        if (!isset($params['order'])) {
            $params['order'] = 'ic.cnt DESC';
        }

        return ProductsTypes::joinAlsoBought($id_products, getVal('count', $params, null));
    }

    public function handleArticleProduct(&$params)
    {
        if (!isset($params['article'])) {
            throw new \InvalidArgumentException('insert_products - article_products: missing parameter "article" - ID of article');
        }

        return function (QueryBuilder $qb) use ($params) {
            $qb->join('p', 'products_in_articles', 'pia', 'pia.id_product = p.id');

            return Op::equals(['pia.id_article' => $params['article']]);
        };
    }

    public function handleAlsoBought(&$params)
    {
        if (empty($params['ID'])) {
            throw new \InvalidArgumentException('insert_products - also_bought: missing "ID" parameter - product ID');
        }

        $params['order'] = 'COUNT(oi.id_product) DESC';

        return function (QueryBuilder $qb) use ($params) {
            $qb->join('p', 'order_items', 'oi', 'p.id = oi.id_product AND oi.total_price > 0')
                ->join('oi', 'orders', 'o', 'o.id = oi.id_order AND o.date_updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)')
                ->join('oi', 'order_items', 'oi2', 'oi.id_order = oi2.id_order')
                ->addParameters([
                    'id_product_0' => $params['ID'],
                    'id_product_1' => $params['ID'],
                ]);

            return 'oi2.id_product = :id_product_0 AND p.id != :id_product_1';
        };
    }

    public function handleAlsoBuyed(&$params)
    {
        return $this->handleAlsoBought($params);
    }

    public function handleRelatedFallback(&$params): callable
    {
        // nactu si sekce a vyberu z toho nahodne max 10 sekci, at mam co nejvic omezenej vyber
        $sections = array_filter(getDescendantCategories(ServiceContainer::getService(ActiveCategory::class)->getCategoryId()));
        if (empty($sections) && !empty($params['products'])) {
            // pokud mam vic produktu
            foreach ($params['products'] as $item) {
                $productSections = [];
                if (is_object($item) && method_exists($item, 'fetchSections')) {
                    $productSections = $item->fetchSections();
                } elseif (isset($item['product']) && method_exists($item['product'], 'fetchSections')) {
                    // kvuli pouziti kosiku
                    $productSections = $item['product']->fetchSections();
                }

                foreach ($productSections as $productSection) {
                    $sections = array_merge($sections, getDescendantCategories($productSection->getId()));
                }
            }
        }

        shuffle($sections);
        $sections = array_slice($sections, 0, 5);

        // chci to random, aby se produkty menily
        $params['random'] = 'very_random';

        $ignoreProductIds = [];
        if (!empty($params['product'])) {
            $ignoreProductIds = [$params['product']->id];
        } elseif (!empty($params['products'])) {
            $ignoreProductIds = array_map(fn ($x) => $x['id'] ?? $x, $params['products']);
        }

        return function (QueryBuilder $qb) use ($sections, $ignoreProductIds) {
            $qb->joinSectionsOnProducts();

            $specs = [Operator::inIntArray($sections, 'ps.id_section')];

            if (!empty($ignoreProductIds)) {
                $specs[] = Operator::not(Operator::inIntArray($ignoreProductIds, 'p.id'));
            }

            return Operator::andX($specs);
        };
    }

    public function handleBestsellingProduct(&$params)
    {
        unset($params['random']); // current implementation of random parameter doesn't make sense with bestselling products

        if (!isset($params['order'])) {
            $params['order'] = 'p.pieces_sold DESC';
        }

        if (isset($params['ID']) || isset($params['product'])) {
            if (isset($params['product']) && ($params['product'] instanceof ProductWrapper || $params['product'] instanceof ProductUnifiedWrapper)) {
                $params['product'] = $params['product']['id'];
            }

            return function (QueryBuilder $qb) use ($params) {
                $qb->addParameters(
                    [
                        'id_product_0' => $params['ID'] ?? $params['product'],
                    ]
                );

                return 'p.id != :id_product_0';
            };
        }
    }

    public function handleLastVisited(&$params)
    {
        if (isset($params['ID']) || isset($params['product'])) {
            $id_product = intval($params['ID'] ?? $params['product']);
        } else {
            $id_product = null;
        }

        $cookie = getVal('last_visited', $_COOKIE, '');
        $delete = getVal('last_visited_delete');

        $ids = array_filter(array_map(function ($x) {
            return intval($x);
        }, explode(',', $cookie)),
            function ($x) use ($id_product, $delete) {
                return $x > 0 && $x != $id_product && $x != $delete;
            });

        if ($id_product) {
            array_unshift($ids, $id_product);
        }
        setCookies('last_visited', join(',', array_slice($ids, 0, 10)), 3600 * 24 * 30);

        if ($ids) {
            $ids_string = join(',', $ids);
            $params['order'] = "FIELD(p.id, {$ids_string})";

            return Op::inIntArray($ids, 'p.id');
        } else {
            return null;
        }
    }

    public function handleLastVisitedStateless(&$params)
    {
        $ids = $params['products'];

        if (!$ids) {
            $ids = [-1];
        }

        $ids_string = join(',', $ids);
        $params['order'] = "FIELD(p.id, {$ids_string})";

        return Op::inIntArray($ids, 'p.id');
    }

    /**
     * Joins `products_in_sections_positions` and apply ordering by product positions in sections.
     */
    public function handleSectionProducts(array &$params): callable
    {
        if (empty($params['section'])) {
            throw new \InvalidArgumentException('insert_products - section_products: missing \'section\' parameter');
        }

        $params['order'] = 'COALESCE(ip_psp.position, 999) ASC';

        return function (QueryBuilder $qb) use ($params) {
            $qb->leftJoin('p', 'products_in_sections_positions', 'ip_psp', 'p.id = ip_psp.id_product AND ip_psp.id_section IN (:insertProductSections)')
              ->setParameter('insertProductSections', $params['section'], Connection::PARAM_INT_ARRAY);
        };
    }

    public function handlePrevProduct(&$params)
    {
        $category = getVal('category', $params);
        if (!$category) {
            throw new \InvalidArgumentException('insert_products - next_product: missing \'category\' parameter - product category to search in');
        }

        $product = getVal('product', $params, getVal('IDproduct'));
        if (!$product) {
            throw new \InvalidArgumentException('insert_products - next_product: missing \'product\' parameter - current product ID');
        }

        if ($product instanceof ProductWrapper || $product instanceof ProductUnifiedWrapper) {
            $product = $product['id'];
        }

        $params['count'] = getVal('count', $params, 1);

        return function (QueryBuilder $qb) use ($params, $category, $product) {
            $compare = '>=';
            $orderDir = 'ASC';
            if ($params['type'] == 'prev_product') {
                $compare = '<';
                $orderDir = 'DESC';
            }

            $qb->orderBy('price', $orderDir)
                ->joinSectionsOnProducts()
                ->setMaxResults($params['count'])
                ->addParameters([
                    'category' => $category,
                    'product' => $product,
                ]);

            return "ps.id_section = :category AND p.id != :product AND p.price {$compare} (SELECT price FROM products WHERE id = :product)";
        };
    }

    public function handleNextProduct(&$params)
    {
        return $this->handlePrevProduct($params);
    }

    public function handleDiscountProduct()
    {
        return function (QueryBuilder $qb) {
            return 'p.discount > 0';
        };
    }

    public function handleLastPieces(array &$params): callable
    {
        return function (QueryBuilder $qb) use ($params) {
            $qb->setParameter('lastPieces_maxPieces', (int) ($params['maxPieces'] ?? 1));

            $storeId = $params['storeId'] ?? null;
            // chci last pieces pouze na konkretnim skladu
            if (findModule(\Modules::STORES) && $storeId) {
                $qb->setParameter('lastPieces_storeId', $storeId);

                if ($storeId == 1) {
                    $qb->setParameter('lastPieces_externalStoreType', StoresInStore::TYPE_EXTERNAL_STORE);
                    $quantityDecrementQb = sqlQueryBuilder()
                        ->select('COALESCE(SUM(si_lp.quantity), 0)')
                        ->from('stores_items', 'si_lp')
                        ->where('si_lp.id_product = p.id AND (si_lp.id_variation = pv.id OR si_lp.id_variation IS NULL AND pv.id IS NULL) AND si_lp.id_store IN (SELECT id FROM stores WHERE type != :lastPieces_externalStoreType)');

                    $storeField = "COALESCE(pv.in_store, p.in_store) - ({$quantityDecrementQb->getSQL()})";
                } else {
                    $storeField = 'SELECT SUM(COALESCE(si_lp.quantity, 0)) FROM stores_items si_lp WHERE si_lp.id_product = p.id AND si_lp.id_variation <=> pv.id AND si_lp.id_store = :lastPieces_storeId';
                }

                return "({$storeField}) > 0 AND ({$storeField}) <= :lastPieces_maxPieces";
            }

            return 'p.in_store > 0 AND p.in_store <= :lastPieces_maxPieces';
        };
    }
}
