<?php

namespace PublishPress\FuturePro\Modules\Workflows\Domain\Engine;

use Closure;
use PublishPress\Future\Core\HookableInterface;
use PublishPress\Future\Framework\Logger\LoggerInterface;
use PublishPress\Future\Modules\Workflows\Domain\Engine\WorkflowEngine as FreeWorkflowEngine;
use PublishPress\Future\Modules\Workflows\HooksAbstract as FreeHooksAbstract;
use PublishPress\Future\Modules\Workflows\Interfaces\ExecutionContextRegistryInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\WorkflowEngineInterface;
use PublishPress\Future\Modules\Workflows\Interfaces\WorkflowModelInterface;
use PublishPress\Future\Modules\Workflows\Models\WorkflowModel;
use PublishPress\FuturePro\Modules\Workflows\HooksAbstract;
use PublishPress\FuturePro\Modules\Workflows\Models\EventDrivenActionModel;
use Throwable;

class WorkflowEngine implements WorkflowEngineInterface
{
    public const LOG_PREFIX = FreeWorkflowEngine::LOG_PREFIX;

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

    /**
     * @var WorkflowEngineInterface
     */
    private $freeEngine;

    /**
     * @var Closure
     */
    private $stepRunnerFactory;

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

    public function __construct(
        HookableInterface $hooks,
        WorkflowEngineInterface $freeEngine,
        Closure $stepRunnerFactory,
        LoggerInterface $logger
    ) {
        $this->hooks = $hooks;
        $this->freeEngine = $freeEngine;
        $this->stepRunnerFactory = $stepRunnerFactory;
        $this->logger = $logger;

        $this->hooks->addFilter(
            FreeHooksAbstract::FILTER_WORKFLOW_ENGINE_MAP_STEP_RUNNER,
            [$this, 'mapStepRunner'],
            10,
            3
        );

        $this->hooks->addFilter(
            FreeHooksAbstract::FILTER_INTERVAL_IN_SECONDS,
            [$this, 'filterIntervalInSeconds'],
            10,
            2
        );

        $this->hooks->addAction(
            HooksAbstract::ACTION_EVENT_DRIVEN_STEP_EXECUTE,
            [$this, "executeEventDrivenStepRoutine"],
            10,
            2
        );
    }

    public function start()
    {
        $this->freeEngine->start();
    }

    public function runWorkflows(array $workflowIdsToExecute = [])
    {
        $this->freeEngine->runWorkflows($workflowIdsToExecute);
    }

    public function setCurrentAsyncActionId($actionId)
    {
        $this->freeEngine->setCurrentAsyncActionId($actionId);
    }

    public function getCurrentAsyncActionId(): int
    {
        return $this->freeEngine->getCurrentAsyncActionId();
    }

    public function mapStepRunner($stepRunner, string $stepName, string $executionContextId)
    {
        $factory = $this->stepRunnerFactory;
        $stepRunner = $factory($stepName, $executionContextId);

        return $stepRunner;
    }

    /**
     * @since 4.4.1
     */
    public function getEngineExecutionId(): string
    {
        return $this->freeEngine->getEngineExecutionId();
    }

    /**
     * @since 4.4.1
     */
    public function prepareExecutionContextForWorkflow(
        string $workflowExecutionId,
        WorkflowModelInterface $workflowModel
    ): void {
        $this->freeEngine->prepareExecutionContextForWorkflow($workflowExecutionId, $workflowModel);
    }

    /**
     * @since 4.4.1
     */
    public function prepareExecutionContextForTrigger(
        string $workflowExecutionId,
        array $triggerStep
    ): void {
        $this->freeEngine->prepareExecutionContextForTrigger($workflowExecutionId, $triggerStep);
    }

    /**
     * @since 4.4.1
     */
    public function generateUniqueId(): string
    {
        return $this->freeEngine->generateUniqueId();
    }

    public function getExecutionContextRegistry(): ExecutionContextRegistryInterface
    {
        return $this->freeEngine->getExecutionContextRegistry();
    }

    /**
     * @since 4.5.0
     */
    public function filterIntervalInSeconds(int $interval, array $nodeSettings)
    {
        $unit = sanitize_key($nodeSettings['schedule']['repeatIntervalUnit'] ?? 'seconds');

        if (empty($unit)) {
            $unit = 'seconds';
        }

        // Convert interval to seconds
        if ($interval > 0) {
            switch ($unit) {
                case 'seconds':
                    $interval *= 1;
                    break;
                case 'minutes':
                    $interval *= MINUTE_IN_SECONDS;
                    break;
                case 'hours':
                    $interval *= HOUR_IN_SECONDS;
                    break;
                case 'days':
                    $interval *= DAY_IN_SECONDS;
                    break;
                case 'weeks':
                    $interval *= WEEK_IN_SECONDS;
                    break;
                case 'months':
                    $interval *= MONTH_IN_SECONDS;
                    break;
                case 'years':
                    $interval *= YEAR_IN_SECONDS;
                    break;
                default:
                    $interval = 0;
                    break;
            }
        }

        return $interval;
    }

    public function executeEventDrivenStepRoutine(int $actionId, array $actionArgs = [])
    {
        // TODO: This is a temporary solution to execute the event driven step routine. We need to move this,
        // or part of it, to the WorkflowEngine.
        try {
            $eventDrivenActionModel = new EventDrivenActionModel();
            $loaded = $eventDrivenActionModel->load($actionId);

            if (! $loaded) {
                $message = self::LOG_PREFIX . ' Event driven action not found';

                throw new \Exception(esc_html($message));
            }

            /**
             * @since 4.7.0
             *
             * Action triggered when the event driven step routine is executed.
             *
             * @param int $actionId The action id.
             * @param array $actionArgs The action args.
             */
            $this->hooks->doAction(
                HooksAbstract::ACTION_WORKFLOW_ENGINE_EXECUTE_EVENT_DRIVEN_STEP,
                $actionId,
                $actionArgs
            );

            $args = $eventDrivenActionModel->args;

            if (empty($args)) {
                $message = self::LOG_PREFIX . ' Event driven step runner error, no args found';

                throw new \Exception(esc_html($message));
            }

            if (! isset($args['runtimeVariables'])) {
                $message = self::LOG_PREFIX . ' Event driven step runner error, no runtime variables found';

                throw new \Exception(esc_html($message));
            }

            if (! isset($args['runtimeVariables']['global']['workflow']['execution_id'])) {
                $message = self::LOG_PREFIX . ' Event driven step runner error, no workflow execution id found';

                throw new \Exception(esc_html($message));
            }

            if (! isset($args['step']['nodeId'])) {
                $message = self::LOG_PREFIX . ' Event driven step runner error, no step node id found';

                throw new \Exception(esc_html($message));
            }

            $workflowId = $args['runtimeVariables']['global']['workflow']['value'];

            $workflow = new WorkflowModel();
            $workflow->load($workflowId);

            $stepId = $args['step']['nodeId'];
            $step = $workflow->getPartialRoutineTreeFromNodeId($stepId);

            if (empty($step)) {
                $message = self::LOG_PREFIX . ' Event driven step runner error, step not found';

                throw new \Exception(esc_html($message));
            }

            $nodeName = $step['node']['data']['name'];
            $workflowExecutionId = $args['runtimeVariables']['global']['workflow']['execution_id'];

            $stepRunner = call_user_func($this->stepRunnerFactory, $nodeName, $workflowExecutionId);

            $this->logger->debug(
                sprintf(
                    self::LOG_PREFIX . '   - Workflow %1$d: Executing event driven step %2$s on action %3$d',
                    $workflowId,
                    $stepId,
                    $actionId
                )
            );

            $executionContextRegistry = $this->getExecutionContextRegistry();
            $executionContext = $executionContextRegistry->getExecutionContext($workflowExecutionId);

            // Expand the runtime variables and set them in the execution context
            $expandedRuntimeVariables = $executionContext->expandRuntimeVariables(
                $args['runtimeVariables']
            );
            $executionContext->setAllVariables($expandedRuntimeVariables);

            $args['actionArgs'] = $actionArgs;

            $stepRunner->actionCallback($args, []);
        } catch (Throwable $e) {
            $this->logger->error(
                sprintf(
                    self::LOG_PREFIX . ' Event driven step runner error: %s. File: %s:%d',
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine()
                )
            );
        }
    }
}
