<?php

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

use PublishPress\Future\Core\HookableInterface;
use PublishPress\Future\Modules\Workflows\Domain\Steps\Triggers\Definitions\OnSchedule;
use PublishPress\Future\Modules\Workflows\HooksAbstract;
use PublishPress\Future\Modules\Workflows\Interfaces\AsyncStepProcessorInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\ExecutionContextInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\TriggerRunnerInterface;

class OnScheduleRunner implements TriggerRunnerInterface
{
    /**
     * The default interval in seconds that the setup should be skipped.
     *
     * @var int
     */
    public const DEFAULT_SETUP_INTERVAL = 30;

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

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

    /**
     * @var AsyncStepProcessorInterface
     */
    private $asyncStepProcessor;

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

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

    public function __construct(
        AsyncStepProcessorInterface $asyncStepProcessor,
        ExecutionContextInterface $executionContext,
        HookableInterface $hooks
    ) {
        $this->asyncStepProcessor = $asyncStepProcessor;
        $this->executionContext = $executionContext;
        $this->hooks = $hooks;

        $this->initialize();
    }

    private function initialize(): void
    {
        $this->hooks->addFilter(
            HooksAbstract::FILTER_SHOULD_USE_TIMESTAMP_ON_ACTION_UID,
            [$this, 'shouldUseTimestampOnActionUID'],
            10,
            3
        );

        $this->hooks->addFilter(
            HooksAbstract::FILTER_SHOULD_SKIP_SCHEDULING,
            [$this, 'shouldSkipScheduling'],
            10,
            6
        );
    }

    /**
     * By not using the timestamp, we can have an action UID
     * that is composed by the workflow ID and the step ID only.
     * This allows to use it to avoid scheduling the same step
     * multiple times.
     */
    public function shouldUseTimestampOnActionUID(
        bool $useTimestamp,
        int $workflowId,
        array $step
    ): bool {
        if (! $this->isRelatedToThisStep($workflowId, $step['node']['id'])) {
            return $useTimestamp;
        }

        return false;
    }

    /**
     * We don't want to schedule this step if it's already scheduled.
     */
    public function shouldSkipScheduling(
        bool $shouldSkip,
        int $workflowId,
        string $stepId,
        string $actionUIDHash,
        ExecutionContextInterface $executionContext,
        array $stepData
    ): bool {
        if (! $this->isRelatedToThisStep($workflowId, $stepId)) {
            return $shouldSkip;
        }

        if ($this->asyncStepProcessor->isScheduled($actionUIDHash)) {
            return true;
        }

        return $shouldSkip;
    }

    private function isRelatedToThisStep(int $workflowId, string $stepId): bool
    {
        return $workflowId === $this->workflowId && $stepId === $this->step['node']['id'];
    }

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

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

        $this->asyncStepProcessor->executeSafelyWithErrorHandling(
            $this->step,
            function ($step) {
                if ($this->shouldSetup()) {
                    $this->asyncStepProcessor->setup($this->step, '__return_true');

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

    /**
     * Check if the setup should be skipped. It ads a delay to the setup,
     * avoiding the check on every request.
     *
     * @return bool
     */
    private function shouldSetup(): bool
    {
        if (empty($this->step)) {
            return false;
        }

        $transientKey = 'publishpressfuture_core_on_schedule_setup_'
            . $this->workflowId
            . '_'
            . $this->step['node']['id'];

        /**
         * @param int $defaultTimeout
         *
         * @return int
         */
        $transientTimeout = $this->hooks->applyFilters(
            HooksAbstract::FILTER_CRON_SCHEDULE_RUNNER_TRANSIENT_TIMEOUT,
            self::DEFAULT_SETUP_INTERVAL
        );

        if (get_transient($transientKey)) {
            return false;
        }

        set_transient($transientKey, true, $transientTimeout);

        return true;
    }

    public function actionCallback(array $compactedArgs, array $originalArgs)
    {
        $this->asyncStepProcessor->actionCallback($compactedArgs, $originalArgs, true);
    }
}
