<?php declare(strict_types=1);
namespace Newland\NeosCommon\Command;

use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\ContentRepository\Domain\Repository\WorkspaceRepository;
use Neos\Flow\Annotations as Flow;
use Newland\NeosCommon\Utility\Scripts;
use Psr\Log\LoggerInterface;
use Neos\Flow\Log\PsrLoggerFactoryInterface;

class NodeCommandController extends AbstractCommandController
{

    /**
     * @var WorkspaceRepository
     * @Flow\Inject()
     */
    protected $workspaceRepository;

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

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

    public function injectLogger(PsrLoggerFactoryInterface $loggerFactory): void
    {
        $this->logger = $loggerFactory->get('newlandNeosCommonNode');
    }

    public function repairAllCommand(): void
    {
        $response = $this->output->ask(
            'This command will now call node:repair for all workspaces, try again on error and'
            . ' discard the whole workspace contents if a retry fails. This is a dangerous operation'
            . ' that may result in data loss. Please confirm this by typing "yes, I understand". ❱ '
        );
        if ($response !== 'yes, I understand') {
            $this->output->outputLine('aborting, doing nothing');
            return;
        }

        $workspaces = $this->workspaceRepository->findAll();
        $withErrors = $this->callNodeRepairForWorkspaces($workspaces->toArray());
        $cleared = $this->clearWorkspacesBecauseOfErrors($withErrors);
        $withErrors = $this->callNodeRepairForWorkspaces($cleared);

        if (\count($withErrors) > 0) {
            $this->outputLine('The following workspaces contain errors even after attempting to clear them.');
            $this->outputLine('You may want to clear them manually using `workspace:discard --workspace ...`');
            $this->outputLine('And `node:repair --workspace ...`');
            foreach ($withErrors as $workspace) {
                $this->outputLine('    - ' . $workspace->getName());
            }
        } else {
            $this->outputLine('No errors during operations.');
            $this->outputLine('If there still are issues try discarding your workspace using');
            $this->outputLine('`workspace:discard --workspace ...` and `node:repair --workspace ...`');
            $this->outputLine('The following workspaces exist:');
            foreach ($workspaces as $workspace) {
                $this->outputLine('    - ' . $workspace->getName());
            }
        }
    }

    /**
     * @param Workspace[] $workspaces
     * @return Workspace[]
     */
    private function clearWorkspacesBecauseOfErrors(array $workspaces): array
    {
        $cleared = [];
        foreach ($workspaces as $workspace) {
            if ($this->clearWorkspaceBecauseOfErrors($workspace)) {
                $cleared[] = $workspace;
            }
        }
        return $cleared;
    }

    /**
     * @param Workspace $workspace
     */
    private function clearWorkspaceBecauseOfErrors(Workspace $workspace): bool
    {
        $question = sprintf(
            'node:repair on workspace %s was unsuccessful.' .
            ' Do you want to discard changes in this workspace? [Y|n] ',
            $workspace->getName()
        );
        if (!$this->output->askConfirmation($question)) {
            return false;
        }

        try {
            $this->callCommand('workspace:discard', [ 'workspace' => $workspace->getName() ]);
            return true;
        } catch (\Exception $e) {
            $this->logger->error($e->getMessage());
            return false;
        }
    }

    /**
     * @param Workspace[] $workspaces
     * @return Workspace[]
     */
    private function callNodeRepairForWorkspaces(array $workspaces): array
    {
        $withErrors = [];

        foreach ($workspaces as $workspace) {
            if (!$this->callNodeRepairForWorkspace($workspace)) {
                $withErrors[] = $workspace;
            }
        }

        return $withErrors;
    }

    private function callNodeRepairForWorkspace(Workspace $workspace): bool
    {
        try {
            $this->callCommand('node:repair', [ 'workspace' => $workspace->getName() ]);
            return true;
        } catch (\Exception $e) {
            $this->logger->error($e->getMessage());
        }

        // Calling node:repair twice sometimes resolves issues that exist when only calling it once.
        $this->output->outputLine('node:repair failed, trying again');
        try {
            $this->callCommand('node:repair', [ 'workspace' => $workspace->getName() ]);
            return true;
        } catch (\Exception $e) {
            $this->logger->error($e->getMessage());
            return false;
        }
    }

    private function callCommand(string $commandName, array $arguments): void
    {
        $output = '$ php flow ' . $commandName;
        foreach ($arguments as $key => $value) {
            $output .= ' --' . $key . '=' . $value;
        }
        $this->outputLine('<i>' . $output . '</i>');

        Scripts::executeInteractiveCommand($commandName, $this->flowSettings, $arguments);
    }
}
