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

/*
 * This file is part of the "toubiz-poi-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\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\FluidAdaptor\Core\Widget\AbstractWidgetController;
use Neos\Neos\Service\LinkingService;
use Neos\Utility\Arrays;
use Newland\NeosCommon\Service\NodeService;
use Newland\Toubiz\Poi\Neos\Domain\Repository\TopicRepository;
use Newland\Toubiz\Poi\Neos\Service\LinkListService;
use Newland\Toubiz\Sync\Neos\Domain\Filter\ArticleFilter;
use Newland\Toubiz\Sync\Neos\Domain\Filter\EventDateFilter;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\CityData;
use Newland\Toubiz\Sync\Neos\Domain\Model\ZipCode;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Domain\Repository\EventDateRepository;
use Newland\Toubiz\Sync\Neos\Exception\MissingDataException;
use Psr\Log\LoggerInterface;

/**
 * The widget controller for the city list widget.
 */
class LinkListController extends AbstractWidgetController
{

    /**
     * @var TopicRepository
     * @Flow\Inject()
     */
    protected $topicRepository;

    /**
     * @var ConfigurationManager
     * @Flow\Inject()
     */
    protected $configurationManager;

    /**
     * @var LinkingService
     * @Flow\Inject()
     */
    protected $linkingService;

    /**
     * @var ArticleRepository
     * @Flow\Inject()
     */
    protected $articleRepository;

    /**
     * @var EventDateRepository
     * @Flow\Inject()
     */
    protected $eventDateRepository;

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

    /**
     * @var LinkListService
     * @Flow\Inject()
     */
    protected $linkListService;

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

    public function indexAction(): void
    {
        $article = $this->widgetConfiguration['article'];
        $node = $this->widgetConfiguration['node'];
        $siteName = $this->nodeService->getCurrentSiteNodeName($node);
        $configuration = $this->linkListService->getCityDetailConfigurationByClient($siteName);

        $links = [];
        if ($configuration && $configuration['enabled'] === true) {
            foreach ($configuration['data'] as $key => $linkConfiguration) {
                $linkData = $this->prepareTargetUrl($linkConfiguration, $article, $siteName);
                if ($linkData) {
                    $links[$key] = $linkData;
                }
            }
        }

        $this->view->assignMultiple(
            [
                'links' => $links,
                'article' => $article,
            ]
        );
    }

    private function prepareTargetUrl(array $linkData, Article $article, string $clientName): ?array
    {
        if (\is_string($linkData['target']) && strpos($linkData['target'], 'settings://') === 0) {
            $linkData['target'] = $this->resolveSettingsReference($linkData['target']);
        }

        if (!$linkData['target']) {
            return null;
        }

        if (strpos($linkData['target'], 'node://') === 0) {
            $queryParameter = $linkData['queryParameters']['city'] ?? 'city';
            $linkData['url'] = $this->getUriFromNode($article, $linkData['target'], $queryParameter);
            if (!$this->hasResults($linkData)) {
                return null;
            }
        } else {
            $linkData['url'] = $this->fillUriPlaceholders($article, $linkData['target']);
        }

        return !empty($linkData['url']) ? $linkData : null;
    }

    private function resolveSettingsReference(string $reference): ?string
    {
        $configurationPath = substr($reference, \strlen('settings://'));

        /** @var mixed $resolved */
        $resolved = $this->configurationManager->getConfiguration(
            ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
            $configurationPath
        );

        if (!\is_string($resolved)) {
            $this->logger->error(sprintf(
                'Referenced setting \'%s\' must be a string, %s found',
                $configurationPath,
                gettype($resolved)
            ));
            return null;
        }

        return (string) $resolved;
    }

    /**
     * @param string $url
     * @param array $replacements
     * @return string
     * @todo Extract to helper package
     *
     */
    private function urlTemplate(string $url, array $replacements): string
    {
        return str_replace(
            array_keys($replacements),
            array_map('urlencode', array_values($replacements)),
            $url
        );
    }

    private function getUriFromNode(Article $article, string $nodeUri, string $queryParameter): ?string
    {
        $baseNode = $this->widgetConfiguration['node'];
        $node = $this->linkingService->convertUriToObject($nodeUri, $baseNode);
        $arguments = (array) Arrays::setValueByPath(
            [],
            $queryParameter,
            [ $article->getPersistenceObjectIdentifier() ]
        );

        try {
            return $this->linkingService->createNodeUri(
                $this->getControllerContext(),
                $node,
                $baseNode,
                null,
                false,
                $arguments
            );
        } catch (\Exception $exception) {
            $this->logger->error($exception->getMessage(), LogEnvironment::fromMethodName(__METHOD__));
            return null;
        }
    }

    private function fillUriPlaceholders(Article $article, string $targetUri): string
    {
        if ($article->getCityData() === null) {
            throw new MissingDataException('City article has no city data attached.', 1561279197);
        }

        $uri = $this->urlTemplate(
            $targetUri,
            $this->getUrlReplacements($article->getCityData())
        );
        return $uri;
    }

    private function getUrlReplacements(CityData $cityData): array
    {
        return [
            '{idToubiz}' => $cityData->getIdToubiz(),
            '{idTomas}' => $cityData->getIdTomas(),
        ];
    }

    private function hasResults(array $linkData): bool
    {
        $type = $linkData['type'] ?? '';

        if ($type === 'article') {
            return $this->checkArticleResults($linkData);
        }

        if ($type === 'event') {
            return $this->checkEventResults($linkData);
        }

        // If the type is unknown, it might be an external link.
        return true;
    }

    private function checkArticleResults(array $linkData): bool
    {
        $filter = new ArticleFilter();

        /** @var Article $city */
        $city = $this->widgetConfiguration['article'];
        $cityData = $city->getCityData();
        if ($cityData) {
            $zipCodes = array_map(
                function (ZipCode $zipCode) {
                    return $zipCode->getZipCode();
                },
                $cityData->getZipCodes()->toArray()
            );
            $filter->setZips($zipCodes);
        }

        if ((string) ($linkData['mainType'] ?? '') !== '') {
            $filter->setMainType($linkData['mainType']);
        }

        $language = $this->getLanguageForNodeUri($linkData['target']);
        $result = $this->articleRepository->withLanguage(
            $language,
            function () use ($filter) {
                return $this->articleRepository->countByFilter($filter, 10);
            }
        );
        return $result['items'] > 0;
    }

    private function checkEventResults(array $linkData): bool
    {
        $filter = new EventDateFilter();

        /** @var Article $city */
        $city = $this->widgetConfiguration['article'];
        $cityData = $city->getCityData();
        if ($cityData) {
            $zipCodes = array_map(
                function (ZipCode $zipCode) {
                    return $zipCode->getZipCode();
                },
                $cityData->getZipCodes()->toArray()
            );
            $filter->setLocationZips($zipCodes);
        }

        $language = $this->getLanguageForNodeUri($linkData['target']);
        $result = $this->eventDateRepository->withLanguage(
            $language,
            function () use ($filter) {
                return $this->eventDateRepository->countByFilter($filter, 10);
            }
        );
        return $result['items'] > 0;
    }

    private function getLanguageForNodeUri($uri): ?string
    {
        /** @var NodeInterface|null $node */
        $node = $this->linkingService->convertUriToObject($uri, $this->widgetConfiguration['node']);
        if ($node) {
            return $node->getDimensions()['language'][0] ?? null;
        }
        return null;
    }
}
