<?php

namespace KupShop\AutomationConfiguratorBundle\Util;

use KupShop\AutomationConfiguratorBundle\Configurator\ConfigurationSaver;
use KupShop\AutomationConfiguratorBundle\Configurator\Generic\Triggers\TestTrigger;
use KupShop\AutomationConfiguratorBundle\Configurator\Interfaces\Conditions\Fields\FieldInterface;
use KupShop\AutomationConfiguratorBundle\Configurator\Interfaces\Config\ConditionsConfig;
use KupShop\AutomationConfiguratorBundle\Configurator\Interfaces\Config\ConfiguratorObject;
use KupShop\AutomationConfiguratorBundle\Configurator\Interfaces\Context\AutomationContextProvider;
use KupShop\AutomationConfiguratorBundle\Event\TriggerEvent;
use KupShop\AutomationConfiguratorBundle\Exceptions\ActionRuntimeException;
use KupShop\AutomationConfiguratorBundle\Exceptions\UnknownTriggerException;
use KupShop\AutomationConfiguratorBundle\JSEvaluator\IJSEvaluator;
use KupShop\AutomationConfiguratorBundle\JSEvaluator\V8ConditionsEval;
use KupShop\AutomationConfiguratorBundle\Logger\AutomationLoggerInterface;
use KupShop\AutomationConfiguratorBundle\Logger\AutomationTestLogger;
use KupShop\KupShopBundle\Util\Database\QueryHint;

class ConfigurationsRunner
{
    /**
     * @var ExpressionContext
     */
    protected $expressionContext;

    /**
     * @var AutomationLoggerInterface
     */
    protected $logger;

    /**
     * @var ConfigurationSaver
     */
    protected $saveConfiguration;

    protected $preFetchContext;

    /**
     * @var AutomationConfiguratorLocator
     */
    protected $automationConfiguratorLocator;

    /**
     * @var IJSEvaluator
     */
    protected $conditionsEval;

    /**
     * @var AutomationTestLogger
     */
    protected $automationTestLogger;

    protected $activeFields;

    /**
     * @required
     */
    public function setAutomationConfiguratorLocator(AutomationConfiguratorLocator $expressionContext): void
    {
        $this->automationConfiguratorLocator = $expressionContext;
    }

    public function setLogger(AutomationLoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    /**
     * @required
     */
    public function setExpressionContext(ExpressionContext $expressionContext): void
    {
        $this->expressionContext = $expressionContext;
    }

    /**
     * @required
     */
    public function setSaveConfiguration(ConfigurationSaver $saveConfiguration): void
    {
        $this->saveConfiguration = $saveConfiguration;
    }

    /**
     * @required
     */
    public function setAutomationTestLogger(AutomationTestLogger $automationTestLogger): void
    {
        $this->automationTestLogger = $automationTestLogger;
    }

    /**
     * @required
     */
    public V8ConditionsEval $v8ConditionsEval;

    /**
     * Finds all configurator objects from context that contain trigger from TriggerEvent.
     *
     * @return ConfiguratorObject[]
     */
    protected function filterConfigurations(TriggerEvent $event, $contextName)
    {
        $return = [];

        $activeTrigger = $this->automationConfiguratorLocator->getTrigger($event->getTriggerName());

        foreach ($this->saveConfiguration->retrieveAll($contextName, $event) as $configurationObject) {
            if (empty($configurationObject->getActionsConfig()) || !$configurationObject->isActive()) {
                continue;
            }
            foreach ($configurationObject->getTriggersConfig() as $triggerConfig) {
                if ($triggerConfig->getName() == $activeTrigger->getName() && $activeTrigger->isRunnable($triggerConfig,
                    $event->getValues())) {
                    $return[] = $configurationObject;
                    break;
                }
            }
        }

        return $return;
    }

    /**
     * Removes group names from the string.
     *
     * @return string
     */
    protected function removeVarsGroupNames(string $string)
    {
        $search = [];
        $replace = [];

        foreach ($this->activeFields as $field) {
            $search[] = $field->getGroupName().'.'.$field->getName();
            $replace[] = $field->getName();
        }

        return str_replace($search, $replace, $string);
    }

    /**
     * Finds first level conditions and calls prefetch fast condition on the condition field.
     */
    protected function recursiveJsonSearchPrefetch($jsonObject)
    {
        if (!empty($jsonObject['and'])) {
            foreach ($jsonObject['and'] as $condition) {
                $foundName = $this->jsonFieldFindName($condition);
                if ($foundName) {
                    $exactName = $this->removeVarsGroupNames($foundName);
                    $fieldsMap = $this->automationConfiguratorLocator->getFieldsServiceMap();

                    $this->preFetchContext = $fieldsMap[$exactName]->modifyPrefetchFastCondition($this->preFetchContext, $condition);
                }
            }
        }
    }

    /**
     * Checks if json object array contains some field name and returns it.
     *
     * @return false|mixed
     */
    protected function jsonFieldFindName($jsonObject)
    {
        if (!empty($jsonObject['var'])) {
            return $jsonObject['var'];
        }

        if (!empty($jsonObject['and']) || !empty($jsonObject['or']) || !empty($jsonObject['!'])) {
            return false;
        }

        $return = false;
        if (is_array($jsonObject)) {
            foreach ($jsonObject as $item) {
                $found = $this->jsonFieldFindName($item);
                if ($found) {
                    $return = $found;
                }
            }
        }

        return $return;
    }

    protected function prefetchExpressionContext(ConfiguratorObject $configuratorObject)
    {
        $jsonObject = $configuratorObject->getConditionsConfig()->getjsonObject();

        if ($jsonObject['logic'] ?? false) {
            $this->recursiveJsonSearchPrefetch($jsonObject['logic']);
        }
    }

    /**
     * Entrypoint for given trigger. Gets all contexts that trigger is associated with.
     * For each context creates prefetch object, retrieves and evaluates all active configurator objects which contain given trigger.
     */
    public function run(TriggerEvent $trigger)
    {
        $contexts = $this->automationConfiguratorLocator->getTriggerContextsName($trigger->getTriggerName());

        foreach ($contexts as $context) {
            $contextProvider = $this->automationConfiguratorLocator->getContextProvider($context);
            $activeConfigurations = $this->filterConfigurations($trigger, $contextProvider::getContextName());

            foreach ($activeConfigurations as $activeConfiguration) {
                $this->preFetchContext = $contextProvider->createRawContext();
                $activeTrigger = $this->automationConfiguratorLocator->getTrigger($trigger->getTriggerName());
                $activeTrigger->modifyPrefetchContext($this->preFetchContext, $trigger->getValues());

                $this->runConfiguration($activeConfiguration, $trigger);
            }
        }
    }

    public function runSingle(ConfiguratorObject $configuratorObject, TriggerEvent $trigger, $contextRestriction = null)
    {
        $contextProvider = $this->automationConfiguratorLocator->getContextProvider($configuratorObject->getContext());
        $this->preFetchContext = $contextProvider->createRawContext();
        if (!empty($contextRestriction)) {
            $this->preFetchContext = $contextProvider->restrictTestContext($this->preFetchContext, $contextRestriction);
        }
        $this->runConfiguration($configuratorObject, $trigger);
    }

    /**
     * Entrypoint for testing configuration object. Returns array of logs.
     *
     * @param $contextRestriction array
     *
     * @return array
     */
    public function runTest($configurationId, $contextRestriction = null, $appData = null)
    {
        $configuratorObject = $this->saveConfiguration->retrieve($configurationId);

        if ($appData) {
            $configuratorObject->setConditionsConfig($this->saveConfiguration->createConditionsConfig($appData['querybuilder']));
            $configuratorObject->setActionsConfig($this->saveConfiguration->createActionsConfig($appData['actions']));
        }
        $this->setLogger($this->automationTestLogger);
        $this->runSingle($configuratorObject, new TriggerEvent(TestTrigger::NAME), $contextRestriction);

        return $this->automationTestLogger->getLogs();
    }

    /**
     * @return FieldInterface[]
     */
    protected function getActiveFields(AutomationContextProvider $contextProvider, ConditionsConfig $conditionsConfig)
    {
        $fields = $this->automationConfiguratorLocator->getFields($contextProvider::getContextName());
        $activeFields = [];

        if (preg_match_all('/(([a-z]|\_)+\.([a-z]|\_)+)/', $conditionsConfig->getJsString(), $fieldsRegexMatches)) {
            foreach ($fields as $field) {
                if (in_array($field->getGroupName().'.'.$field->getName(), array_unique($fieldsRegexMatches[1]))) {
                    $activeFields[] = $field;
                }
            }
        }

        return $activeFields;
    }

    protected function runConfiguration(ConfiguratorObject $configuratorObject, TriggerEvent $triggerEvent)
    {
        $test = $triggerEvent->getTriggerName() === TestTrigger::NAME;

        try {
            $trigger = $this->automationConfiguratorLocator->getTrigger($triggerEvent->getTriggerName());
        } catch (UnknownTriggerException $e) {
            $trigger = null;
        }

        $conditionsConfig = $configuratorObject->getConditionsConfig();
        $contextProvider = $this->automationConfiguratorLocator->getContextProvider($configuratorObject->getContext());

        $this->activeFields = $this->getActiveFields($contextProvider, $conditionsConfig);

        foreach ($this->activeFields as $field) {
            $field->modifyPrefetchOnUse($this->preFetchContext);
        }

        $this->prefetchExpressionContext($configuratorObject);

        $this->preFetchContext = $contextProvider->filterNotExecuted($configuratorObject, $this->preFetchContext);

        $maxFetchLimit = $this->getLimitFromTriggerAndContext($trigger?->getDbFetchLimit(), $contextProvider->getDbFetchLimit());

        if ($trigger?->shouldQueriesGoToMaster()) {
            $fetchedContext = QueryHint::withRouteToMaster(fn () => $contextProvider->fetchContext($this->preFetchContext, $maxFetchLimit));
        } else {
            $fetchedContext = $contextProvider->fetchContext($this->preFetchContext, $maxFetchLimit);
        }

        if (!$test && count($fetchedContext) >= $maxFetchLimit) {
            $this->logger->logError("Autopilot s ID {$configuratorObject->getId()} vybral příliš mnoho objektů z databáze (více než {$maxFetchLimit}), proto se neprovedl. Optimalizujete jeho nastavení.",
                [
                    'ID' => $configuratorObject->getId(),
                    'limit' => count($fetchedContext),
                ]);

            return;
        }

        $maxFilteredLimit = $this->getLimitFromTriggerAndContext($trigger?->getMaxFilteredObjectsLimit(), $contextProvider->getMaxFilteredObjectsLimit());

        $this->prepareConditionsEvaluator();

        $this->conditionsEval->init();
        $this->conditionsEval->run($conditionsConfig->getJsString() ?: 'true', array_values($fetchedContext), $this->activeFields, $maxFilteredLimit);
        $passedIds = $this->conditionsEval->getResult();

        $contextToExecute = array_filter(
            $fetchedContext,
            function ($key) use ($passedIds) {  return in_array($key, $passedIds); },
            ARRAY_FILTER_USE_KEY
        );

        if (!$test && count($contextToExecute) >= $maxFilteredLimit) {
            $this->logger->logError("Pro autopilot s ID {$configuratorObject->getId()} splnilo podmínky příliš mnoho objektů (více než {$maxFilteredLimit}), proto se neprovedl. Optimalizujete jeho nastavení.",
                [
                    'ID' => $configuratorObject->getId(),
                    'limit' => count($contextToExecute),
                ]);

            return;
        }

        // do not spam activityLog
        $skipSingleLogs = !$test && count($contextToExecute) > 1000;

        $actionsConfig = $configuratorObject->getActionsConfig();

        foreach ($contextToExecute as $fetchedContextItem) {
            $this->expressionContext->setContext($fetchedContextItem);

            if (!$test) {
                $contextProvider->addExecutionHistory($configuratorObject, $fetchedContextItem);
            }

            foreach ($actionsConfig as $actionConfig) {
                $actionsMap = $this->automationConfiguratorLocator->getActionsServiceMap($contextProvider::getContextName());
                if (!$test) {
                    try {
                        if (!$actionsMap[$actionConfig->getName()]->run($actionConfig, $fetchedContextItem, $triggerEvent)) {
                            continue;
                        }
                    } catch (ActionRuntimeException $e) {
                        $this->logger->logError($e->getMessage(), $e->getData());
                        continue;
                    }
                }

                if (!$skipSingleLogs && ($logMessage = $actionsMap[$actionConfig->getName()]->getLog($actionConfig, $fetchedContextItem))) {
                    $this->logger->log($logMessage);
                }
            }
        }

        if ($test || $configuratorObject->isDetailLog() || $skipSingleLogs) {
            $this->logger->logStatistics($configuratorObject->getName(), count($fetchedContext), count($contextToExecute), $maxFetchLimit, $maxFilteredLimit);
        }
    }

    protected function getLimitFromTriggerAndContext(int|false|null $triggerLimit, int $contextLimit): int
    {
        if ($triggerLimit === false) {
            return PHP_INT_MAX;
        }

        return max($triggerLimit ?? 0, $contextLimit);
    }

    protected function prepareConditionsEvaluator()
    {
        if ($this->conditionsEval) {
            return;
        }

        $this->setConditionsEval($this->v8ConditionsEval);
    }

    public function setConditionsEval(IJSEvaluator $conditionsEval): void
    {
        $this->conditionsEval = $conditionsEval;
    }
}
