<?php

namespace KupShop\RestrictionsBundle\Utils;

use KupShop\RestrictionsBundle\Entity\RestrictionsFilterParams;
use Query\Filter;
use Query\Operator as Op;
use Query\Product;

class Restrictions
{
    /** @var array */
    private $restrictionSpec;

    /** @var \Raven_Client */
    private $raven;

    public const TYPE_HIDE = 'H';
    public const TYPE_SHOW = 'S';

    protected $cfg = [
        'Modules' => [
            'restrictions' => [
                'objects' => [
                    'categories' => 1,
                    'producers' => 2,
                    'campaigns' => 3,
                    'products' => 4,
                    'parameters_list' => 5,
                ],
                'domains' => [
                    'countries' => 1,
                    'users' => 2,
                    'login' => 4,
                    'price_levels' => 5,
                    'users_groups' => 6,
                ],
            ],
        ],
    ];

    public function getRestrictionSpec()
    {
        // v adminu - v detailu objednavky ignorujeme restrictions
        if (isAdministration()) {
            return null;
        }

        if ($this->restrictionSpec === null) {
            $this->restrictionSpec = $this->createSpec();
        }

        return $this->restrictionSpec;
    }

    /**
     * @return \Query
     */
    public function getRestrictionSpecSnippet()
    {
        $qb = sqlQueryBuilder();

        $query = new \Query();
        $query->where = (string) $qb->evaluateClosures([$this->getRestrictionSpec()])[0];
        $query->data = $qb->getParameters();
        $query->types = $qb->getParameterTypes();

        return $query;
    }

    /**
     * @return array
     */
    public function getDbQuery()
    {
        $qb = sqlQueryBuilder()->select('r.id, (r.domain_invert = "Y") AS invert, r.object_value, r.type')
            ->from('restrictions', 'r')
            ->join('r', 'restrictions_domains', 'rd', 'r.id = rd.id_restriction')
            ->groupBy('r.id');

        $find = [];
        $user = \User::getCurrentUser();

        $find[] = 'WHEN 4 THEN FIND_IN_SET(:login, GROUP_CONCAT(rd.login))';
        $qb->addParameters(['login' => ($user ? 'logIn' : 'logOut')]);

        // Při registraci v košíku Future uživatel neexistuje a nemáme o něm informace a proto id > 0
        if ($user && $user->id > 0) {
            $find[] = 'WHEN 1 THEN FIND_IN_SET(:country, GROUP_CONCAT(rd.country))';
            $qb->addParameters(['country' => $user->country]);

            $find[] = 'WHEN 2 THEN FIND_IN_SET(:user, GROUP_CONCAT(rd.id_user))';
            $qb->addParameters(['user' => $user->id]);

            if (findModule(\Modules::PRICE_LEVELS) && $user->getUserPriceLevelId()) {
                $find[] = 'WHEN 5 THEN FIND_IN_SET(:price_level, GROUP_CONCAT(rd.id_price_level))';
                $qb->addParameters(['price_level' => $user->getUserPriceLevelId()]);
            }

            if ($user->getGroups()) {
                $find_groups = [];
                foreach ($user->getGroups() as $group) {
                    $find_groups[] = "FIND_IN_SET(:group{$group['id']}, GROUP_CONCAT(rd.id_users_group))";
                    $qb->addParameters(["group{$group['id']}" => $group['id']]);
                }
                $find_groups = implode('+', $find_groups);
                $find[] = "WHEN 6 THEN {$find_groups}";
            }
        }

        return [$qb, $find];
    }

    private function getDbRules(): array
    {
        [$qb, $find] = $this->getDbQuery();

        $cacheKey = $this->getCacheKey($qb->getParameters());
        if ($cachedRules = getCache($cacheKey)) {
            return $cachedRules;
        }

        $find = '(CASE r.domain 
            '.implode('
            ', $find).'
           ELSE 0
           END) AS find';
        $qb->addSelect($find);

        $qb->having('(invert XOR find)');

        $ruleRows = $qb->execute()->fetchAllAssociative();
        setCache($cacheKey, $ruleRows, 30 * 60);

        return $ruleRows;
    }

    /**
     * @param array $specs default implicit specs
     *
     * @return callable
     */
    protected function createSpec($specs = [])
    {
        $ruleRows = $this->getDbRules();

        $typeSpecs = [];

        foreach ($ruleRows as $ruleRow) {
            $ruleArrays = json_decode_strict($ruleRow['object_value'], true);
            $rowSpecs = $this->getRuleRowSpecs($ruleArrays);

            if (!$rowSpecs) {
                continue;
            }

            switch ($ruleRow['type']) {
                case self::TYPE_HIDE:
                    $typeSpecs[self::TYPE_HIDE][] = Op::not(Op::andX($rowSpecs));
                    break;
                case self::TYPE_SHOW:
                    $typeSpecs[self::TYPE_SHOW][] = Op::andX($rowSpecs);
                    break;
            }
        }

        if ($typeSpecs[self::TYPE_HIDE] ?? false) {
            $specs[] = Op::andX($typeSpecs[self::TYPE_HIDE]);
        }
        if ($typeSpecs[self::TYPE_SHOW] ?? false) {
            $specs[] = Op::orX($typeSpecs[self::TYPE_SHOW]);
        }

        if ($specs) {
            return Op::andX($specs);
        }

        return function () {
            return '1';
        };
    }

    private function getRuleRowSpecs($ruleArrays)
    {
        $rowSpecs = [];
        $objectNames = array_flip($this->getObjectNames());

        foreach ($ruleArrays as $objectId => $ruleArray) {
            $objectName = $objectNames[$objectId];
            $ruleArray = array_values($ruleArray);

            if (empty($ruleArray)) {
                // skip empty rules (when restrictions are incorrectly imported)
                continue;
            }

            $specName = 'restrict_'.$objectName;
            if (method_exists($this, $specName)) {
                $rowSpecs[] = function () use ($specName, $ruleArray) {
                    return call_user_func([$this, $specName], $ruleArray);
                };
            } else {
                $this->getRaven()->captureMessage(
                    "No spec found to restrict results by '%s'.",
                    [$objectName]
                );
            }
        }

        return $rowSpecs;
    }

    public function getRestrictionFilterParams(): RestrictionsFilterParams
    {
        $ruleRows = $this->getDbRules();
        $filterParamsByType = [];

        foreach ($ruleRows as $ruleRow) {
            $ruleArrays = json_decode_strict($ruleRow['object_value'], 1);
            $filterParams = $this->getRuleRowFilterParams($ruleArrays);

            if (!$filterParams) {
                continue;
            }

            $filterParamsByType[$ruleRow['type']][] = $filterParams;
        }

        return new RestrictionsFilterParams($filterParamsByType[self::TYPE_HIDE] ?? [], $filterParamsByType[self::TYPE_SHOW] ?? []);
    }

    private function getRuleRowFilterParams($ruleArrays)
    {
        $filterParams = new \FilterParams();

        $objectNames = array_flip($this->getObjectNames());

        foreach ($ruleArrays as $objectId => $ruleArray) {
            $objectName = $objectNames[$objectId];
            $ruleArray = array_values($ruleArray);
            switch ($objectName) {
                case 'categories':
                    $filterParams->setSections($ruleArray);
                    break;
                case 'producers':
                    $filterParams->setProducers($ruleArray);
                    break;
                case 'campaigns':
                    foreach ($ruleArray as $campaignId) {
                        $filterParams->setCampaign($campaignId);
                    }
                    break;
                case 'products':
                    $filterParams->setProducts($ruleArray);
                    break;
                case 'parameters_list':
                    $paramsListParamIds = sqlQueryBuilder()->select('id, id_parameter')->from('parameters_list')
                        ->where(Op::inIntArray($ruleArray, 'id'));

                    $paramsIndex = [];
                    foreach ($paramsListParamIds->execute() as $param) {
                        $paramsIndex[$param['id_parameter']][] = $param['id'];
                    }
                    foreach ($paramsIndex as $paramId => $paramValues) {
                        $filterParams->setParameter($paramId, $paramValues);
                    }

                    break;
            }
        }

        return $filterParams;
    }

    public function getObjectNames()
    {
        return array_merge($this->cfg['Modules']['restrictions']['objects'], findModule('restrictions', 'objects', []));
    }

    public function getDomainNames()
    {
        return array_merge($this->cfg['Modules']['restrictions']['domains'], findModule('restrictions', 'domains', []));
    }

    protected function restrict_categories(array $categoryIds)
    {
        return Product::inSections($categoryIds, 'rps');
    }

    protected function restrict_producers(array $producerIds)
    {
        return Op::andX(
            Filter::byProducers($producerIds),
            'pr.id is not null'
        );
    }

    protected function restrict_campaigns(array $campaigns)
    {
        return Filter::byCampaigns($campaigns, 'OR');
    }

    protected function restrict_products(array $productIds)
    {
        return Op::inIntArray($productIds, 'p.id');
    }

    protected function restrict_parameters_list(array $parameterIds)
    {
        $filterCallback = function ($alias, $paramId, $paramData) {
            return Op::equals(["{$alias}.value_list" => $paramData]);
        };

        $parameters = array_combine($parameterIds, $parameterIds);

        return Filter::parametersFilterUtil($parameters, $filterCallback, false);
    }

    /**
     * @return \Raven_Client
     */
    public function getRaven()
    {
        if (!$this->raven) {
            $this->raven = getRaven();
        }

        return $this->raven;
    }

    public function setRaven(\Raven_Client $raven)
    {
        $this->raven = $raven;
    }

    /**
     * @param $query array containing at least 'data', 'where' and 'types' keys
     *
     * @return array modified query
     */
    public function applyToOldQuery($query)
    {
        $restrictions = $this->getRestrictionSpecSnippet();

        $query['data'] = array_merge(
            $query['data'],
            $restrictions->data
        );
        $query['types'] = array_merge(
            $query['types'],
            $restrictions->types
        );
        $query['where'] .= " AND ({$restrictions->where})";

        return $query;
    }

    private function getCacheKey(array $parameters): string
    {
        $cacheKey = '';
        foreach ($parameters as $key => $value) {
            $cacheKey .= '_'.$key.'-'.$value;
        }

        return 'restrictions'.$cacheKey;
    }
}
