<?php
namespace Newland\Toubiz\Sync\Neos\Command;

/*
 * This file is part of the "neos-common" package.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

use Doctrine\ORM\EntityManagerInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Booting\Scripts;
use Neos\Flow\Log\ThrowableStorageInterface;
use Neos\Flow\Mvc\Exception\StopActionException;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Flow\Validation\ValidatorResolver;
use Newland\NeosCommon\Service\ObjectPathMappingService;
use Newland\Toubiz\Api\Constants\Language;
use Newland\Toubiz\Sync\Neos\Command\Helper\ConfigurationHelper;
use Newland\Toubiz\Sync\Neos\Logging\LoggerFactory;
use Newland\Toubiz\Sync\Neos\Utility\SimpleFilebasedLock;
use Psr\Log\LoggerInterface;

class AbstractCommandController extends \Newland\NeosCommon\Command\AbstractCommandController
{
    use UsesLockFile;

    /**
     * Number of entities after which the current EntityManager state should be cleaned.
     *
     * Smaller sizes may decrease memory footprint but increase runtime,
     * Larger sizes may increase memory footprint but decrease runtime as cached
     * values can be used instead of refetching them.
     *
     * @var int
     */
    protected $cleanStateAfter = 20;

    /**
     * @Flow\InjectConfiguration
     * @var array
     */
    protected $configuration;

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

    /**
     * @var LoggerFactory
     */
    protected $loggerFactory;

    /**
     * @var ThrowableStorageInterface
     * @Flow\Inject()
     */
    protected $exceptionLogger;

    /**
     * @var ObjectPathMappingService
     * @Flow\Inject()
     */
    protected $objectPathMappingService;

    /**
     * @Flow\Inject()
     * @var PersistenceManagerInterface
     */
    protected $persistenceManager;

    /**
     * @var EntityManagerInterface
     * @Flow\Inject()
     */
    protected $entityManager;

    /**
     * @var ValidatorResolver
     * @Flow\Inject()
     */
    protected $validatorResolver;

    /**
     * @Flow\InjectConfiguration(package="Neos.Flow")
     * @var array
     */
    protected $flowSettings;

    /**
     * @var ConfigurationHelper
     * @Flow\Inject(lazy=FALSE)
     */
    protected $configurationHelper;

    public function __construct()
    {
        parent::__construct();

        $this->on(
            'finish',
            function () {
                $this->persistenceManager->persistAll();
                $this->outputLine('Flushing cache');
                Scripts::executeCommand('neos.flow:cache:flush', $this->flowSettings);
                $this->outputLine();
                $this->outputLine('Warming up cache');
                Scripts::executeCommand('neos.flow:cache:warmup', $this->flowSettings);
                $this->outputLine();
            }
        );

        register_shutdown_function(
            function (): void {
                $error = error_get_last();
                if ($error) {
                    // phpcs:disable Generic.PHP.ForbiddenFunctions.Found
                    var_dump('A fatal, uncatchable error occurred during import:');
                    var_dump($error);
                    // phpcs:enable
                }
            }
        );
    }

    public function injectLogger(LoggerFactory $loggerFactory): void
    {
        $this->loggerFactory = $loggerFactory;
        $this->logger = $loggerFactory->getLogger();
    }

    protected function callCommandMethod(): void
    {
        try {
            $cliArguments = $GLOBALS['argv'];
            unset($cliArguments[0]);
            $commandCalled = implode(' ', $cliArguments);
            $this->loggerFactory->updateName($commandCalled);
            $this->logger->info($commandCalled, $this->request->getArguments());

            $lockPath = $this->getLockPath();

            $lock = new SimpleFilebasedLock($lockPath);
            $lock->setLogger($this->logger);

            $lock->synchronize(
                function () {
                    parent::callCommandMethod();
                }
            );
        } catch (\Throwable $e) {
            $this->logger->error($this->exceptionLogger->logThrowable($e));
            throw $e;
        }
    }

    /**
     * Find configuration for given service string.
     *
     * @param string $service
     * @return array|null
     */
    protected function getConfigurationForService($service): ?array
    {
        if ($this->configuration['services'][$service] ?? null) {
            $configuration = $this->configuration['services'][$service];
            $configuration['languages'] = $configuration['languages'] ?? [ Language::DE ];
            return $configuration;
        }

        return null;
    }

    protected function wrapImportClosure(callable $block): callable
    {
        $calls = 0;
        return function () use ($block, &$calls) {
            try {
                $block(...\func_get_args());
                $this->persistenceManager->persistAll();
                if (++$calls % $this->cleanStateAfter === 0) {
                    $this->persistenceManager->clearState();
                    $this->validatorResolver->reset();
                }
            } catch (\Throwable $e) {
                $this->logger->critical($this->exceptionLogger->logThrowable($e));
                if (!$this->entityManager->isOpen()
                    || $this->objectManager->getContext()->isTesting()
                ) {
                    throw $e;
                }
            }
        };
    }

    protected function askForConfirmationAndAbortIfNoneGiven(string $message, bool $default = false): void
    {
        $options = $default ? '[Y|n]' : '[y|N]';
        $message .= ' ' . $options . ' ';
        if (!$this->output->askConfirmation($message, $default)) {
            $this->output->outputLine('Aborting: Action not confirmed');
            throw new StopActionException('Action not confirmed.');
        }
    }

    /**
     * @Flow\Signal()
     * @param string $type
     * @param string[]|null $flushedIds
     */
    protected function emitFlush(string $type, array $flushedIds = null): void
    {
        $this->callListeners('flush', $type, [ $flushedIds ]);
    }

    /**
     * @Flow\Signal()
     */
    protected function emitFinish(): void
    {
        $this->callListeners('finish');
    }
}
