<?php declare(strict_types = 1);

namespace idoit\Module\SyneticsFlows\Automation;

use DateTime;
use idoit\Module\SyneticsFlows\Automation\Action\ExecutionFactory\ExecutionFactory;
use idoit\Module\SyneticsFlows\Automation\Action\ExecutionResult\DebugResult;
use idoit\Module\SyneticsFlows\Automation\Action\ExecutionResult\ErrorExecutionResult;
use idoit\Module\SyneticsFlows\Automation\Action\ExecutionResult\ExecutionResult;
use idoit\Module\SyneticsFlows\Automation\Action\PerformExecution\PerformExecution;
use idoit\Module\SyneticsFlows\Automation\Trigger\Invocation\Invocation;
use idoit\Module\SyneticsFlows\Automation\Trigger\Invocation\InvocationContext;
use idoit\Module\SyneticsFlows\Automation\Trigger\InvocationMatcher\InvocationMatcher;
use idoit\Module\SyneticsFlows\Automation\Trigger\TriggerType\ScheduleTriggerType;
use idoit\Module\SyneticsFlows\Model\ExecutionDao;
use idoit\Module\SyneticsFlows\Session\UserSwitcher;
use mysqli_sql_exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class TriggerService
{
    private const RETRY_COUNT = 3;

    public function __construct(
        private InvocationMatcher $matcher,
        private ExecutionDao $dao,
        private ExecutionFactory $factory,
        private PerformExecution $performExecution,
        private UserSwitcher $userSwitcher
    )
    {
    }

    public function findAutomations(Invocation $invocation): array
    {
        if ($invocation->getContext() === null) {
            $invocation->setContext(InvocationContext::fromEnvironment());
        }
        $results = [];
        foreach ($this->matcher->findAutomations($invocation) as $automation) {
            $results[] = $automation;
        }
        return $results;
    }

    public function invoke(Invocation $invocation): array
    {
        if ($invocation->getContext() === null) {
            $invocation->setContext(InvocationContext::fromEnvironment());
        }
        $currentDateTime = (new DateTime('now'))->format(DateTime::ATOM);
        $created = [];

        /** @var Automation $automation */
        foreach ($this->findAutomations($invocation) as $automation) {
            if ($automation->getTrigger() instanceof ScheduleTriggerType && $automation->getCreatedBy()) {
                $invocation->getContext()->setUserId((string)$automation->getCreatedBy());
            }
            foreach ($automation->getActions() as $action) {
                if (!$this->factory->supports($action)) {
                    continue;
                }
                $info = $this->factory->createExecution($invocation, $automation, $action);

                $trigger = $automation->getTrigger();
                $now = $trigger->getNextStartDate()?->format(DateTime::ATOM) ?? $currentDateTime;

                if ($info) {
                    $status = 'waiting';
                    $result = null;
                    if ($automation->getStatus() === 'debug') {
                        $status = 'debug';
                        $result = new DebugResult($info);
                    }
                    $execution = new Execution(null, $automation->getId(), $automation->getName(), $now, null, null, $status, $invocation, $info, $result);
                    $created[] = $this->dao->save($execution);
                }
            }
        }

        return $created;
    }

    public function perform(string $id, int $retry = 0): ?ExecutionResult
    {
        $autocommit = $this->dao->retrieve("SELECT @@autocommit")->get_row_value('@@autocommit');
        try {
            $this->dao->begin_update();
            $execution = $this->dao->get($id, ExecutionDao::LOCK_MODE_READ);
            if (!$execution) {
                $this->dao->cancel_update();
                throw new NotFoundHttpException('Cannot find execution');
            }
            if ($execution->getStatus() === 'debug') {
                $this->dao->cancel_update();
                return new DebugResult($execution->getExecution());
            }

            if ($execution->getStatus() !== 'waiting') {
                $this->dao->cancel_update();
                return null;
            }

            $execution->start();
            $this->dao->save($execution);
        } catch (mysqli_sql_exception $e) {
            if ($retry > self::RETRY_COUNT) {
                return new ErrorExecutionResult("Cannot perform after $retry retries: {$e->getMessage()}");
            }
            return $this->perform($id, $retry + 1);
        } finally {
            $this->dao->set_autocommit($autocommit === '1');
        }

        $result = null;

        $this->userSwitcher->runAs(
            function () use (&$result, $execution) {
                $result = $this->performExecution->perform($execution);
            },
            (int)$execution->getInvocation()->getContext()->getUserId()
        );

        $execution->setResult($result);
        $this->dao->save($execution);
        return $result;
    }
}
