<?php
namespace Newland\Toubiz\Poi\Neos\Service;

/*
 * 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 Doctrine\ORM\AbstractQuery;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\I18n\Translator;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Media\Domain\Model\AssetInterface;
use Neos\Neos\Service\LinkingService;
use Neos\Utility\Arrays;
use Newland\NeosCommon\Service\NodeService;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\CityData;
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;

/**
 * @Flow\Scope("singleton")
 */
class LinkListService
{
    /**
     * @var array
     * @Flow\InjectConfiguration(package="Newland.Toubiz.Poi.Neos", path="cityDetails.links")
     */
    protected $linksConfiguration;

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

    /**
     * @var array
     * @Flow\InjectConfiguration(package="Newland.Toubiz.Poi.Neos", path="linkTargets.sites")
     */
    protected $listLinkTargets;

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

    /**
     * @var LoggerInterface
     * @Flow\Inject()
     */
    protected $logger;

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

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

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

    /**
     * @var Translator
     * @Flow\Inject()
     */
    protected $translator;

    public function getLinkTargetNodeByClientAndArticleType(int $articleType, string $clientName = 'default'): string
    {
        $configuration = $this->listLinkTargets[$clientName]['lists'] ??
            $this->listLinkTargets['default']['lists'] ?? '';
        return $configuration[$articleType] ?? '';
    }

    public function getCityDetailConfigurationByClient(string $clientName): ?array
    {

        $clientConfiguration = $this->linksConfiguration['sites'][$clientName] ?? [];
        $defaultConfiguration = $this->linksConfiguration['sites']['default'];

        $clientConfiguration = (array) array_replace_recursive($defaultConfiguration, $clientConfiguration);

        if (array_key_exists('enabled', $clientConfiguration) &&
            !$clientConfiguration['enabled']) {
            return null;
        }

        return $clientConfiguration;
    }

    /**
     * @param ControllerContext $controllerContext
     */
    public function getLinks(NodeInterface $node, Article $article, $controllerContext): array
    {
        $siteName = $this->nodeService->getCurrentSiteNodeName($node);
        $configuration = $this->getCityDetailConfigurationByClient($siteName);
        $links = [];
        if (($configuration['enabled'] ?? false) === true) {
            foreach ($configuration['data'] as $key => $linkConfiguration) {
                $linkData = $this->prepareTargetUrl($linkConfiguration, $article, $node, $controllerContext);
                if ($linkData) {
                    $linkData['label'] = $this->translator->translateById(
                        sprintf('link.%s', $linkData['labelId']),
                        [],
                        null,
                        null,
                        'Views/Cities',
                        'Newland.Toubiz.Poi.Neos'
                    );
                    $links[$key] = $linkData;
                }
            }
        }

        return $links;
    }

    /**
     * @param ControllerContext $controllerContext
     */
    private function prepareTargetUrl(
        array $linkData,
        Article $article,
        NodeInterface $baseNode,
        $controllerContext
    ): ?array {
        if (str_starts_with(($linkData['target'] ?? ''), 'settings://')) {
            $linkData['target'] = $this->resolveSettingsReference($linkData['target']);
        }

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

        if (str_starts_with($linkData['target'], 'node://')) {
            $queryParameter = $linkData['queryParameters']['city'] ?? 'city';
            $linkData['url'] = $this->getUriFromNode(
                $article,
                $linkData['target'],
                $queryParameter,
                $baseNode,
                $controllerContext
            );

            if (!$this->hasResults($linkData, $article, $baseNode)) {
                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 ControllerContext $controllerContext
     */
    private function getUriFromNode(
        Article $article,
        string $nodeUri,
        string $queryParameter,
        NodeInterface $baseNode,
        $controllerContext
    ): ?string {
        $node = $this->linkingService->convertUriToObject($nodeUri, $baseNode);
        $arguments = (array) Arrays::setValueByPath(
            [],
            $queryParameter,
            [ $article->getPersistenceObjectIdentifier() ]
        );

        try {
            return $this->linkingService->createNodeUri(
                $controllerContext,
                $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);
        }

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

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

    private function hasResults(array $linkData, Article $article, NodeInterface $node): bool
    {
        $type = $linkData['type'] ?? '';

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

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

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

    private function checkArticleResults(array $linkData, Article $city, NodeInterface $node): bool
    {
        $language = $this->getLanguageForNodeUri($linkData['target'], $node);

        $query = $this->articleRepository->withLanguage(
            $language,
            function () use ($city, $linkData) {
                $newQuery = $this->articleRepository->createQueryBuilder('article');
                $newQuery->andWhere($newQuery->expr()->eq('article.mainType', $linkData['mainType']));
                $newQuery
                    ->leftJoin('article.cities', 'city')
                    ->andWhere(
                        $newQuery->expr()->eq(
                            'city',
                            $newQuery->expr()->literal($city->getPersistenceObjectIdentifier())
                        )
                    );
                return $newQuery;
            }
        );

        $totalCount = $query
                ->select('COUNT(article) as count')
                ->getQuery()
                ->execute([], AbstractQuery::HYDRATE_ARRAY)[0]['count'] ?? 0;

        return $totalCount > 0;
    }

    private function checkEventResults(array $linkData, Article $city, NodeInterface $node): bool
    {
        $language = $this->getLanguageForNodeUri($linkData['target'], $node);

        $query = $this->eventDateRepository->withLanguage(
            $language,
            function () use ($city) {
                $newQuery = $this->eventDateRepository->createQueryBuilder('eventDates');
                $newQuery
                    ->leftJoin('eventDates.event', 'event')
                    ->leftJoin('event.cities', 'city')
                    ->andWhere(
                        $newQuery->expr()->eq(
                            'city',
                            $newQuery->expr()->literal($city->getPersistenceObjectIdentifier())
                        )
                    );
                return $newQuery;
            }
        );

        $totalCount = $query
                ->select('COUNT(event) as count')
                ->getQuery()
                ->execute([], AbstractQuery::HYDRATE_ARRAY)[0]['count'] ?? 0;

        return $totalCount > 0;
    }


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

    /**
     * @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
        );
    }
}
