<?php declare(strict_types=1);
namespace Newland\Toubiz\Events\Neos\Controller;

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

use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
use Neos\ContentRepository\Exception\PageNotFoundException;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
use Neos\Flow\Mvc\Routing\ObjectPathMapping;
use Neos\Flow\Mvc\Routing\ObjectPathMappingRepository;
use Neos\Flow\Utility\Now;
use Neos\Neos\Domain\Service\ContentContext;
use Newland\NeosCommon\Logging\DeprecationLog;
use Newland\Toubiz\Events\Neos\Service\EventUrlService;
use Newland\Toubiz\Sync\Neos\Domain\Model\Event;
use Newland\Toubiz\Sync\Neos\Domain\Repository\EventRepository;
use Newland\Toubiz\Sync\Neos\Translation\CurrentLanguageService;

class RedirectController extends AbstractActionController
{
    /**
     * @var EventUrlService
     * @Flow\Inject()
     */
    protected $eventUrlService;

    /**
     * @var ObjectPathMappingRepository
     * @Flow\Inject()
     */
    protected $objectPathMappingRepository;

    /**
     * @var EventRepository
     * @Flow\Inject()
     */
    protected $eventRepository;

    /**
     * @var ContextFactoryInterface
     * @Flow\Inject()
     */
    protected $contextFactory;

    /**
     * @var DeprecationLog
     * @Flow\Inject()
     */
    protected $deprecationLog;

    /**
     * @var CurrentLanguageService
     * @Flow\Inject()
     */
    protected $currentLanguageService;

    public function eventDetailAction(Node $node, string $uriSegment, ?string $language = null): void
    {
        $this->currentLanguageService->updateLanguageFromNode($node);
        $event = $this->findPossibleRedirectionTarget($uriSegment, $language);

        if ($event === null) {
            $uri = sprintf(
                '/redirect/content/event/%s/%s',
                $uriSegment,
                $language
            );
            $this->redirectToUri($uri, 0, 301);
        }

        $currentSiteNode = $this->getCurrentSiteNode($event->getLanguage());
        if ($currentSiteNode === null) {
            throw new PageNotFoundException(
                sprintf(
                    'No site node can be resolved for event %s',
                    $event->getPersistenceObjectIdentifier()
                )
            );
        }

        $uri = $this->eventUrlService->generateUrlByCurrentNode($event, $currentSiteNode);
        $this->deprecationLog->logDeprecation('Deprecated event URL ' . $uriSegment . ' redirected to ' . $uri);
        $this->redirectToUri($uri, 0, 301);
    }

    public function eventListAction(Node $node, string $language): void
    {
        $this->currentLanguageService->updateLanguageFromNode($node);
        $uri = sprintf(
            '/redirect/list/event/%s?params=%s',
            $language,
            $this->request->getMainRequest()->getArgument('params')
        );
        $this->redirectToUri($uri, 0, 301);
    }

    private function findPossibleRedirectionTarget(string $uriSegment, ?string $language): ?Event
    {
        return $this->findEventBasedOnIncorrectPath($uriSegment)
            ?? $this->findEventBasedOnMissingUrlIdentifier($uriSegment)
            ?? $this->findEventBasedOnPersistenceObjectIdentifier($uriSegment)
            ?? $this->findEventBasedOnOriginalId($uriSegment, $language);
    }

    private function findEventBasedOnIncorrectPath(string $uriSegment): ?Event
    {
        $parts = (array) preg_split('/\W/', $uriSegment);
        $potentialUrlIdentifier = $parts[count($parts) - 1];
        return $this->eventRepository->withoutLanguageHandling(
            function () use ($potentialUrlIdentifier) {
                return $this->eventRepository->findOneBy([ 'urlIdentifier' => $potentialUrlIdentifier ]);
            }
        );
    }

    private function findEventBasedOnMissingUrlIdentifier(string $uriSegment): ?Event
    {
        $query = $this->objectPathMappingRepository->createQuery();

        $parts = [];
        $parts[] = $query->logicalAnd(
            [
                $query->equals('objectType', Event::class),
                $query->equals('uriPattern', '{title}-{urlIdentifier}'),
                $query->like('pathSegment', $uriSegment . '%'),
            ]
        );
        $parts[] = $query->logicalAnd(
            [
                $query->equals('objectType', Event::class),
                $query->equals('uriPattern', '{urlIdentifier}-{title}'),
                $query->like('pathSegment', '%' . $uriSegment),
            ]
        );

        $result = $query->matching($query->logicalOr($parts))
            ->execute()
            ->toArray();

        return $this->getEventWithBestMatchingMapping($result);
    }

    private function findEventBasedOnPersistenceObjectIdentifier(string $uriSegment): ?Event
    {
        return $this->eventRepository->withoutLanguageHandling(
            function () use ($uriSegment) {
                return $this->eventRepository->findByIdentifier($uriSegment);
            }
        );
    }

    private function findEventBasedOnOriginalId(string $uriSegment, ?string $language): ?Event
    {
        return $this->eventRepository->withLanguage(
            $language,
            function () use ($uriSegment) {
                return $this->eventRepository->findOneByOriginalId($uriSegment);
            }
        );
    }

    protected function getCurrentSiteNode(?string $language): ?Node
    {
        $dimensions = $language ? [ 'language' => [ $language ] ] : [];
        $context = $this->contextFactory->create(
            [
                'workspaceName' => 'live',
                'currentDateTime' => new Now(),
                'dimensions' => $dimensions,
                'invisibleContentShown' => false,
                'removedContentShown' => false,
                'inaccessibleContentShown' => false,
            ]
        );

        if (!($context instanceof ContentContext)) {
            return null;
        }

        $node = $context->getCurrentSiteNode();
        if (!($node instanceof Node)) {
            return null;
        }

        return $node;
    }

    private function getEventWithBestMatchingMapping(array $result): ?Event
    {
        if (count($result) === 1) {
            /** @var ObjectPathMapping|null $mapping */
            $mapping = $result[0];
            $identifier = $mapping->getIdentifier();
            return $this->eventRepository->withoutLanguageHandling(
                function () use ($identifier) {
                    return $this->eventRepository->findByIdentifier($identifier);
                }
            );
        }

        if (count($result) > 1) {
            $clientFilter = $this->settings['clientFilter'] ?? null;
            if (empty($clientFilter)) {
                throw new InvalidConfigurationException(
                    'The URI is missing an identifier hash but matches more than 1 record. If this is due to' .
                    ' conflicting client imports, please add a `clientFilter` configuration.'
                );
            }

            $identifiers = array_map(
                static function (ObjectPathMapping $mapping) {
                    return $mapping->getIdentifier();
                },
                $result
            );
            $events = $this->eventRepository->withoutLanguageHandling(
                function () use ($identifiers) {
                    return $this->eventRepository->findByIdentifiers($identifiers);
                }
            );

            $siteNodeName = $this->getCurrentSiteNodeName();
            if ($siteNodeName === null) {
                return $events[0];
            }

            $currentClient = $clientFilter[$siteNodeName] ?? null;
            if ($currentClient === null) {
                return $events[0];
            }

            /** @var Event $event */
            foreach ($events as $event) {
                if ($event->getClient() === $currentClient) {
                    return $event;
                }
            }

            return $events[0];
        }

        return null;
    }

    private function getCurrentSiteNodeName(): ?string
    {
        $currentSiteNode = $this->getCurrentSiteNode(null);
        if (!$currentSiteNode) {
            return null;
        }

        $nodeName = $currentSiteNode->getNodeName();
        if (!$nodeName) {
            return null;
        }

        return $nodeName->__toString();
    }
}
