<?php

namespace PublishPress\FuturePro\Modules\Workflows\Domain\Steps\Triggers\Runners;

use PublishPress\Future\Core\HookableInterface;
use PublishPress\Future\Modules\Workflows\Domain\Engine\VariableResolvers\IntegerResolver;
use PublishPress\Future\Modules\Workflows\Domain\Engine\VariableResolvers\PostResolver;
use PublishPress\Future\Modules\Workflows\Interfaces\InputValidatorsInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\StepProcessorInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\TriggerRunnerInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\ExecutionContextInterface;
use PublishPress\Future\Framework\Logger\LoggerInterface;
use PublishPress\Future\Modules\Workflows\Domain\Engine\VariableResolvers\StringResolver;
use PublishPress\Future\Modules\Workflows\Domain\Steps\Triggers\Definitions\OnPostMetaChange;
use PublishPress\Future\Modules\Workflows\Interfaces\WorkflowExecutionSafeguardInterface;
use PublishPress\FuturePro\Modules\Workflows\HooksAbstract;

class OnPostMetaChangeRunner implements TriggerRunnerInterface
{
    private const META_ACTION_ADDED = 'added';

    private const META_ACTION_UPDATED = 'updated';

    private const META_ACTION_DELETED = 'deleted';

    /**
     * @var HookableInterface
     */
    private $hooks;

    /**
     * @var array
     */
    private $step;

    /**
     * @var StepProcessorInterface
     */
    private $stepProcessor;

    /**
     * @var InputValidatorsInterface
     */
    private $postQueryValidator;

    /**
     * @var int
     */
    private $workflowId;

    /**
     * @var ExecutionContextInterface
     */
    private $executionContext;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var array
     */
    private $oldMeta = [];

    /**
     * @var \Closure
     */
    private $expirablePostModelFactory;

    /**
     * @var WorkflowExecutionSafeguardInterface
     */
    private $executionSafeguard;

    public function __construct(
        HookableInterface $hooks,
        StepProcessorInterface $stepProcessor,
        InputValidatorsInterface $postQueryValidator,
        ExecutionContextInterface $executionContext,
        LoggerInterface $logger,
        \Closure $expirablePostModelFactory,
        WorkflowExecutionSafeguardInterface $executionSafeguard
    ) {
        $this->hooks = $hooks;
        $this->stepProcessor = $stepProcessor;
        $this->postQueryValidator = $postQueryValidator;
        $this->executionContext = $executionContext;
        $this->logger = $logger;
        $this->expirablePostModelFactory = $expirablePostModelFactory;
        $this->executionSafeguard = $executionSafeguard;
    }

    public static function getNodeTypeName(): string
    {
        return OnPostMetaChange::getNodeTypeName();
    }

    public function setup(int $workflowId, array $step): void
    {
        $this->step = $step;
        $this->workflowId = $workflowId;

        $metaType = 'post';

        $this->hooks->addFilter(
            sprintf(HooksAbstract::FILTER_UPDATE_METADATA, $metaType),
            [$this, 'saveCacheOfOldMeta'],
            5,
            3
        );

        $this->hooks->addFilter(
            sprintf(HooksAbstract::FILTER_UPDATE_METADATA_BY_MID, $metaType),
            [$this, 'saveCacheOfOldMetaByMid'],
            5,
            4
        );

        $this->hooks->addAction(
            sprintf(HooksAbstract::ACTION_UPDATED_META, $metaType),
            [$this, 'triggerUpdatedMetaCallback'],
            10,
            4
        );

        $this->hooks->addAction(
            sprintf(HooksAbstract::ACTION_ADDED_META, $metaType),
            [$this, 'triggerAddedMetaCallback'],
            10,
            4
        );

        $this->hooks->addAction(
            sprintf(HooksAbstract::ACTION_DELETED_META, $metaType),
            [$this, 'triggerDeletedMetaCallback'],
            10,
            4
        );
    }

    private function checkIfMetaKeyIsInStepSettings($metaKey)
    {
        $metaKeys = isset($this->step['node']['data']['settings']['metaKeys'])
            ? $this->step['node']['data']['settings']['metaKeys'] : [];

        if (is_string($metaKeys)) {
            $metaKeys = explode(',', $metaKeys);
        }

        return in_array($metaKey, $metaKeys);
    }

    public function saveCacheOfOldMetaByMid($check, $metaId, $metaValue, $metaKey)
    {
        $objectType = 'post';

        $meta = get_metadata_by_mid($objectType, $metaId);

        if (! $meta) {
            return $check;
        }

        $column = sanitize_key($objectType . '_id');
        $postId = (int) $meta->{$column};

        return $this->saveCacheOfOldMeta($check, $postId, $metaKey);
    }

    public function saveCacheOfOldMeta($check, $postId, $metaKey)
    {
        if (! $this->checkIfMetaKeyIsInStepSettings($metaKey)) {
            return $check;
        }

        $metaValue = get_post_meta($postId, $metaKey, true);

        if (! isset($this->oldMeta[$postId])) {
            $this->oldMeta[$postId] = [];
        }

        $this->oldMeta[$postId][$metaKey] = $metaValue;

        return $check;
    }

    public function triggerUpdatedMetaCallback($metaId, $postId, $metaKey, $metaValue)
    {
        $this->triggerCallback(self::META_ACTION_UPDATED, $metaId, $postId, $metaKey, $metaValue);
    }

    public function triggerAddedMetaCallback($metaId, $postId, $metaKey, $metaValue)
    {
        $this->triggerCallback(self::META_ACTION_ADDED, $metaId, $postId, $metaKey, $metaValue);
    }

    public function triggerDeletedMetaCallback($metaId, $postId, $metaKey, $metaValue)
    {
        $this->triggerCallback(self::META_ACTION_DELETED, $metaId, $postId, $metaKey, $metaValue);
    }

    private function normalizeStringVariable($value)
    {
        if (is_array($value)) {
            return implode(',', $value);
        }

        return $value;
    }

    private function triggerCallback($action, $metaId, $postId, $metaKey, $metaValue)
    {
        if (! $this->checkIfMetaKeyIsInStepSettings($metaKey)) {
            return;
        }

        $nodeSlug = $this->stepProcessor->getSlugFromStep($this->step);

        if ($this->shouldAbortExecution($nodeSlug)) {
            return;
        }

        $cachedOldBValue = $this->oldMeta[$postId][$metaKey] ?? '';

        if ($action === self::META_ACTION_DELETED) {
            $cachedOldBValue = $metaValue;
            $metaValue = '';
        }

        $post = get_post($postId);

        $this->executionContext->setVariable($nodeSlug, [
            'post' => new PostResolver($post, $this->hooks, '', $this->expirablePostModelFactory),
        ]);

        $postQueryArgs = [
            'post' => $post,
            'node' => $this->stepProcessor->getNodeFromStep($this->step),
        ];

        if (! $this->postQueryValidator->validate($postQueryArgs)) {
            return false;
        }

        $this->stepProcessor->executeSafelyWithErrorHandling(
            $this->step,
            function ($step, $postId, $metaKey, $metaValue, $metaId, $action, $cachedOldBValue) {
                $nodeSlug = $this->stepProcessor->getSlugFromStep($step);

                $post = get_post($postId);

                $metaId = $this->normalizeStringVariable($metaId);
                $cachedOldBValue = $this->normalizeStringVariable($cachedOldBValue);
                $metaValue = $this->normalizeStringVariable($metaValue);

                $this->executionContext->setVariable(
                    $nodeSlug,
                    [
                        'post' => new PostResolver($post, $this->hooks),
                        'postId' => new IntegerResolver($postId),
                        'action' => new StringResolver($action),
                        'metaId' => new IntegerResolver($metaId),
                        'metaKey' => new StringResolver($metaKey),
                        'metaValue' => new StringResolver($metaValue ?? ''),
                        'oldMetaValue' => new StringResolver($cachedOldBValue),
                    ]
                );

                $this->executionContext->setVariable('global.trigger.postId', $postId);

                $this->stepProcessor->triggerCallbackIsRunning();

                $this->logger->debug(
                    $this->stepProcessor->prepareLogMessage(
                        'Trigger is running | Slug: %s | Post ID: %d',
                        $nodeSlug,
                        $postId
                    )
                );

                $this->hooks->doAction(
                    HooksAbstract::ACTION_WORKFLOW_TRIGGER_EXECUTED,
                    $this->workflowId,
                    $this->step
                );

                $this->stepProcessor->runNextSteps($step);
            },
            $postId,
            $metaKey,
            $metaValue,
            $metaId,
            $action,
            $cachedOldBValue
        );
    }

    private function shouldAbortExecution(string $stepSlug): bool
    {
        if (
            $this->executionSafeguard->detectInfiniteLoop(
                $this->executionContext,
                $this->step
            )
        ) {
            $this->logger->debug(
                $this->stepProcessor->prepareLogMessage(
                    'Infinite loop detected for step %s, skipping',
                    $stepSlug
                )
            );

            return true;
        }

        return false;
    }
}
