<?php declare(strict_types=1);

namespace Newland\Toubiz\Poi\Neos\Controller;

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\NeosCommon\Service\NodeService;
use Newland\NeosCommon\Utility\UrlUtility;
use Newland\Toubiz\Poi\Neos\Service\ArticleUrlService;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Service\UrlIdentifierRedirectService;
use Newland\Toubiz\Sync\Neos\Translation\CurrentLanguageService;

class RedirectController extends AbstractActionController
{

    /**
     * @var ArticleUrlService
     * @Flow\Inject()
     */
    protected $articleUrlService;

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

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

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

    /**
     * @var UrlIdentifierRedirectService
     * @Flow\Inject()
     */
    protected $urlIdentifierRedirect;

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

    /**
     * @var NodeService
     * @Flow\Inject()
     */
    protected $nodeService;

    /**
     * @var UrlUtility
     * @Flow\Inject()
     */
    protected $urlUtility;

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

        // Article is not imported. We refer to the widget then.
        if ($article === null) {
            $uri = sprintf(
                '/redirect/content/article/%s/%s',
                $uriSegment,
                $language
            );
            $uri = $this->urlUtility->appendTrackingParameters($uri, $this->request->getMainRequest()->getArguments());
            $this->redirectToUri($uri, 0, 301);
        }

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

        $uri = $this->articleUrlService->generateUrlByCurrentNode($article, $currentSiteNode);
        $uri = $this->urlUtility->appendTrackingParameters($uri, $this->request->getMainRequest()->getArguments());
        $this->redirectToUri($uri, 0, 301);
    }

    public function collectionDetailAction(Node $node, string $uriSegment, string $language): void
    {
        $this->currentLanguageService->updateLanguageFromNode($node);

        // Collection are not being imported. We refer to the widget then.
        $uri = sprintf(
            '/redirect/content/collection/%s/%s',
            $uriSegment,
            $language,
        );
        $this->redirectToUri($uri, 0, 301);
    }

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

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

    private function findPossibleRedirectionTarget(string $uriSegment, ?string $language): ?Article
    {
        return $this->findArticleBasedOnIncorrectPath($uriSegment)
            ?? $this->findArticleBasedOnMissingUrlIdentifier($uriSegment)
            ?? $this->findArticleBasedOnPersistenceObjectIdentifier($uriSegment)
            ?? $this->findArticleBasedOnOriginalId($uriSegment, $language);
    }

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

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

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

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

    private function findArticleBasedOnIncorrectPath(string $uriSegment): ?Article
    {
        return $this->articleRepository->withoutLanguageHandling(
            function () use ($uriSegment) {
                $parts = (array) preg_split('/\W/', $uriSegment);
                $potentialUrlIdentifiers = $this->urlIdentifierRedirect->getRedirects($parts[count($parts) - 1]);
                foreach ($potentialUrlIdentifiers as $potentialUrlIdentifier) {
                    $article = $this->articleRepository->findOneBy([ 'urlIdentifier' => $potentialUrlIdentifier ]);
                    if ($article) {
                        return $article;
                    }
                }
                return null;
            }
        );
    }

    private function findArticleBasedOnPersistenceObjectIdentifier(string $uriSegment): ?Article
    {
        return $this->articleRepository->withoutLanguageHandling(
            function () use ($uriSegment) {
                return $this->articleRepository->findByIdentifier($uriSegment);
            }
        );
    }

    private function findArticleBasedOnOriginalId(string $uriSegment, ?string $language): ?Article
    {
        return $this->articleRepository->withLanguage(
            $language,
            function () use ($uriSegment) {
                return $this->articleRepository->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 getArticleWithBestMatchingMapping(array $result): ?Article
    {
        if (count($result) === 1) {
            /** @var ObjectPathMapping|null $mapping */
            $mapping = $result[0];
            $identifier = $mapping->getIdentifier();
            return $this->articleRepository->withoutLanguageHandling(
                function () use ($identifier) {
                    return $this->articleRepository->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
            );
            $articles = $this->articleRepository->withoutLanguageHandling(
                function () use ($identifiers) {
                    return $this->articleRepository->findByIdentifiers($identifiers);
                }
            );

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

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

            /** @var Article $article */
            foreach ($articles as $article) {
                $currentClient = $currentClientConfiguration['mainType']['values'][$article->getMainType()] ?? null;
                if ($article->getClient() === $currentClient) {
                    return $article;
                }
            }

            return $articles[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();
    }
}
