<?php
namespace Newland\Toubiz\Sync\Neos\Command;

/*
 * This file is part of the "toubiz-sync-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\Flow\Annotations as Flow;
use Neos\Flow\Core\Booting\Scripts;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Api\Service\Outdooractive;
use Newland\Toubiz\Api\Service\Outdooractive\ObjectAdapter\TourAdapter;
use Newland\Toubiz\Api\Service\ServiceFactory;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\CityApiService;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\DbService;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\DirectMarketerApiService;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\GastronomyApiService;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\ObjectAdapter\CityApiService\CityAdapter;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\ObjectAdapter\DbService\PointOfInterestAdapter;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\ObjectAdapter\DirectMarketerApiService\DirectMarketerAdapter;
use Newland\Toubiz\Api\Service\Toubiz\Legacy\ObjectAdapter\GastronomyApiService\GastronomyAdapter;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Enum\ArticleType;
use Newland\Toubiz\Sync\Neos\Domain\Repository\GeometryRepository;
use Newland\Toubiz\Sync\Neos\Importer\ArticleImporter;
use Newland\Toubiz\Sync\Neos\Service\UuidPredictionService;
use Symfony\Component\Console\Output\NullOutput;

/**
 * Articles command controller.
 *
 * Provides commands to manipulate article data.
 *
 * @Flow\Scope("singleton")
 */
class ArticlesCommandController extends AbstractCommandController
{
    protected static $mainTypeMap = [
        ArticleType::POI => ArticleConstants::TYPE_ATTRACTION,
        ArticleType::GASTRONOMY => ArticleConstants::TYPE_GASTRONOMY,
        ArticleType::TOURS => ArticleConstants::TYPE_TOUR,
        ArticleType::LODGINGS => ArticleConstants::TYPE_LODGING,
        ArticleType::DIRECT_MARKETERS => ArticleConstants::TYPE_DIRECT_MARKETER,
        ArticleType::CONGRESS_LOCATIONS => ArticleConstants::TYPE_CONGRESS_LOCATION,
        ArticleType::CITIES => ArticleConstants::TYPE_CITY,
    ];

    const TYPE_FLUSH = 'ARTICLES';

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

    /**
     * @var GeometryRepository
     * @Flow\Inject()
     */
    protected $geometryRepository;

    /**
     * Synchronize command.
     *
     * Updates local articles database from API data source.
     *
     * @param bool $importGeometryIfMissing
     * @param bool $finishHooks
     * @param bool $quiet
     * @param string $only Can be one article type of [poi, gastronomy, lodgings, tours, directMarketers]
     * @param int $minutesToLive The age of records in minutes after which they are considered "old" and will be
     *     deleted. Default is 7 days (=10080 minutes) ago.
     * @param int $limit Limit the amount of articles per type and language.
     * @return void
     * @throws \Exception
     */
    public function synchronizeCommand(
        bool $importGeometryIfMissing = true,
        bool $finishHooks = true,
        bool $quiet = false,
        string $only = '',
        int $minutesToLive = 10080,
        int $limit = null
    ): void {
        if (!$quiet) {
            $this->showProgressOnCommandLine();
        } else {
            $this->output->setOutput(new NullOutput());
        }
        if ($importGeometryIfMissing) {
            $this->importGeometry();
        }
        $articleTypes = ArticleType::values();
        if ($only !== '') {
            $articleTypes = [ $only ];
        }

        foreach ($articleTypes as $currentArticleType) {
            $this->synchronizeArticleType($currentArticleType, $limit);
            $mainType = static::$mainTypeMap[$currentArticleType];
            $deletedCount = $this->deleteOldRecords($minutesToLive, $mainType);
            $this->outputLine(sprintf("\nDeleted %d %s.", $deletedCount, $currentArticleType));
        }


        if ($finishHooks) {
            $this->flushObjectPathMappings();
            $this->emitFinish();
        }
    }

    /**
     * Removes articles from the system according to the given clause.
     * If no WHERE clause is given then all articles will be deleted.
     *
     * # Remove all articles
     * $ php flow articles:remove
     *
     * # Remove articles according to WHERE clause
     * $ php flow articles:remove --where='article.client="westlicherbodensee"'
     *
     * # Remove single article
     * $ php flow articles:remove \
     *      --where="article.Persistence_Object_Identifier='a4625eb4-6e84-4834-969d-c9f1d447408b'"
     *
     *
     * @param string|null $where DQL WHERE clause selecting the articles to delete.
     * @param string|null $language Language to delete articles in. Deletes in all languages if not specified.
     */
    public function removeCommand(string $where = null, string $language = null): void
    {
        if ($where) {
            $this->outputLine('Deleting articles WHERE ' . $where);
        } else {
            $this->outputLine('Deleting all articles');
        }

        $this->showProgressOnCommandLine();
        if ($language === null) {
            $this->articleRepository->withoutLanguageHandling(function () use ($where) {
                $this->remove($where, null);
            });
        } else {
            $this->remove($where, $language);
        }
    }

    public function remove(?string $where, ?string $language): void
    {
        $this->articleRepository->setLanguage($language);
        $query = $this->articleRepository->createQueryBuilder('article');
        if ($where) {
            $query->where($where);
        }
        $count = (clone $query)->select('COUNT(article) AS count')
                ->getQuery()
                ->execute([], AbstractQuery::HYDRATE_ARRAY)[0]['count'] ?? 0;
        $this->askForConfirmationAndAbortIfNoneGiven(sprintf('Do you really want to remove %d articles?', $count));
        $this->emitStart(sprintf('Remove %d articles', $count), compact('language'));

        $processed = 0;
        foreach ($query->getQuery()->iterate() as $articles) {
            foreach ($articles as $article) {
                $this->articleRepository->remove($article);
                $this->emitProgress(sprintf('Remove %d articles', $count), ++$processed, compact('language'));
                if ($processed % 250 === 0) {
                    $this->persistenceManager->persistAll();
                    $this->persistenceManager->clearState();
                    $this->validatorResolver->reset();
                }
            }
        }

        $this->emitEnd(sprintf('Remove %d articles', $count), compact('language'));
    }

    protected function synchronizePointOfInterestsFromDbService(
        $configuration,
        string $language,
        int $limit = null
    ): void {
        $this->emitStart(ArticleType::POI, compact('language'));

        /** @var DbService $service */
        $service = ServiceFactory::get('Toubiz/Legacy/Db', $configuration['baseUri'] ?? null);
        $service->setClientName($configuration['client']);
        $service->setLogger($this->logger);
        $service->setApiKey($configuration['apiKey']);
        $service->setLanguage($language);
        $service->setLimit($limit);

        $minLevelOfMaintenance = $configuration['minLevelOfMaintenance'] ?? 0;

        $processed = 0;
        $service->fetchPointOfInterests(
            $this->wrapImportClosure(
                function (PointOfInterestAdapter $record) use ($minLevelOfMaintenance, &$processed, $language) {
                    $this->emitProgress(ArticleType::POI, ++$processed, compact('language'));
                    if ($record->getLevelOfMaintenance() > $minLevelOfMaintenance) {
                        (new ArticleImporter())->import($record);
                    } else {
                        (new ArticleImporter())->remove($record);
                    }
                }
            )
        );

        $this->emitEnd(ArticleType::POI, compact('language'));
    }

    protected function synchronizeGastronomyFromApi($configuration, string $language, int $limit = null): void
    {
        $this->emitStart(ArticleType::GASTRONOMY, compact('language'));

        /** @var GastronomyApiService $service */
        $service = ServiceFactory::get('Toubiz/Legacy/GastronomyApi', $configuration['baseUri'] ?? null);
        $service->setApiKey($configuration['apiKey']);
        $service->setLanguage($language);
        $service->setLimit($limit);

        $processed = 0;
        $service->fetchGastronomy(
            $this->wrapImportClosure(
                function (GastronomyAdapter $record) use (&$processed, $language) {
                    $this->emitProgress(ArticleType::GASTRONOMY, ++$processed, compact('language'));
                    $importer = new ArticleImporter();
                    if ($record->getOnlineStatus()) {
                        $importer->import($record);
                    } else {
                        $importer->remove($record);
                    }
                }
            )
        );

        $this->emitEnd(ArticleType::GASTRONOMY, compact('language'));
    }

    protected function synchronizeToursFromApi(array $configuration, string $language, int $limit = null): void
    {
        /** @var Outdooractive\ApiService $service */
        $service = ServiceFactory::get('Outdooractive/Api', $configuration['baseUri'] ?? null);
        $service->setClientName($configuration['client']);
        $service->setApiKey($configuration['apiKey']);
        $service->setLanguage($language);
        $service->setLimit($limit);

        $this->emitStart(ArticleType::TOURS, compact('language'));

        $processed = 0;
        $service->fetchTours(
            $this->wrapImportClosure(
                function (TourAdapter $tour) use (&$processed, $language) {
                    $this->emitProgress(ArticleType::TOURS, ++$processed, compact('language'));
                    (new ArticleImporter())->import($tour);
                }
            )
        );

        $this->emitEnd(ArticleType::TOURS, compact('language'));
    }

    protected function synchronizeLodgingsFromTportal(array $configuration, ?string $language, int $limit = null): void
    {
        $client = $configuration['client'];
        $this->emitStart(ArticleType::LODGINGS, compact('client', 'language'));

        /** @var \Newland\Toubiz\Api\Service\Tportal\ApiService $service */
        $service = ServiceFactory::get('Tportal/Api', $configuration['baseUri'] ?? null);
        $service->setClientName($client);
        $service->setLogger($this->logger);
        $service->setLimit($limit);
        $service->setLanguage($language);
        $service->setDetailUriTemplate($configuration['detailUri'] ?? null);

        $processed = 0;
        $service->fetchLodgings(
            $this->wrapImportClosure(
                function ($record) use ($client, &$processed, $language) {
                    $this->emitProgress(ArticleType::LODGINGS, ++$processed, compact('client', 'language'));
                    $importer = new ArticleImporter;
                    $importer->setClient($client);
                    $importer->import($record);
                }
            )
        );

        $this->emitEnd(ArticleType::LODGINGS, compact('client', 'language'));
    }

    protected function synchronizeDirectMarketersFromApi(
        array $configuration,
        string $language,
        int $limit = null
    ): void {
        $this->emitStart(ArticleType::DIRECT_MARKETERS, compact('language'));

        /** @var DirectMarketerApiService $service */
        $service = ServiceFactory::get('Toubiz/Legacy/DirectMarketerApi', $configuration['baseUri'] ?? null);
        $service->setApiKey($configuration['apiKey']);
        $service->setLanguage($language);
        $service->setLimit($limit);

        $processed = 0;
        $service->fetchDirectMarketers(
            $this->wrapImportClosure(
                function (DirectMarketerAdapter $record) use (&$processed, $language) {
                    $this->emitProgress(ArticleType::DIRECT_MARKETERS, ++$processed, compact('language'));
                    $importer = new ArticleImporter;
                    if ($record->getOnlineStatus()) {
                        $importer->import($record);
                    } else {
                        $importer->remove($record);
                    }
                }
            )
        );

        $this->emitEnd(ArticleType::DIRECT_MARKETERS, compact('language'));
    }

    protected function synchronizeCongressLocationsFromTportal(
        array $configuration,
        ?string $language,
        int $limit = null
    ): void {
        $this->emitStart(ArticleType::CONGRESS_LOCATIONS, compact('language'));
        $client = $configuration['client'];

        /** @var \Newland\Toubiz\Api\Service\Tportal\ApiService $service */
        $service = ServiceFactory::get('Tportal/Api', $configuration['baseUri'] ?? null);
        $service->setClientName($client);
        $service->setLogger($this->logger);
        $service->setLimit($limit);
        $service->setLanguage($language);
        $service->setDetailUriTemplate($configuration['detailUri'] ?? null);

        $processed = 0;
        $service->fetchCongressLocations(
            $this->wrapImportClosure(
                function ($record) use ($client, &$processed, $language) {
                    $this->emitProgress(ArticleType::CONGRESS_LOCATIONS, ++$processed, compact('language'));
                    $importer = new ArticleImporter;
                    $importer->setClient($client);
                    $importer->import($record);
                }
            )
        );

        $this->emitEnd(ArticleType::CONGRESS_LOCATIONS, compact('language'));
    }

    protected function synchronizeCitiesFromApi(array $configuration, string $language, int $limit = null): void
    {
        $this->emitStart(ArticleType::CITIES, compact('language'));

        /** @var CityApiService $service */
        $service = ServiceFactory::get('Toubiz/Legacy/CityApi', $configuration['baseUri'] ?? null);
        $service->setApiKey($configuration['apiKey']);
        $service->setClientName($configuration['client']);
        $service->setLogger($this->logger);
        $service->setLanguage($language);
        $service->setLimit($limit);

        $processed = 0;

        $uuidPredictionService = new UuidPredictionService();
        $uuidPredictionService->setDefaultProperties(
            [
                // Cities display related lists of attractions.
                // Attractions do not store the client right now.
                'client' => '',
                'language' => $language,
                'mainType' => ArticleConstants::TYPE_ATTRACTION,
            ]
        );

        $service->fetchCities(
            $this->wrapImportClosure(
                function (CityAdapter $record) use (&$processed, $language, $uuidPredictionService) {
                    $this->emitProgress(ArticleType::CITIES, ++$processed, compact('language'));
                    $importer = new ArticleImporter;
                    $importer->setUuidPredictionService($uuidPredictionService);
                    $importer->import($record);
                }
            )
        );

        $this->emitEnd(ArticleType::CITIES, compact('language'));
    }

    private function synchronizeArticleType(string $type, int $limit = null): void
    {
        if (ArticleType::validValue($type) === false) {
            $message = sprintf('Unknown article type "%s". Valid types are: ', $type)
                . implode(', ', ArticleType::values());
            throw new \Exception($message);
        }

        $method = 'synchronizeArticleType' . ucfirst($type);
        $this->$method($limit);
    }

    private function deleteOldRecords(int $minutesToLive, int $mainType = null): int
    {
        $importer = new ArticleImporter;
        $timeToLive = new \DateInterval(sprintf('PT%dM', $minutesToLive));
        return $importer->deleteOldRecords($timeToLive, $mainType);
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypeLodgings(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Tportal/Api');
        if ($this->hasClients($configuration)) {
            foreach ($this->getClients($configuration) as $clientConfiguration) {
                foreach ($clientConfiguration['languages'] ?? [ 'de' ] as $language) {
                    $this->synchronizeLodgingsFromTportal($clientConfiguration, $language, $limit);
                }
            }
        }
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypePoi(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Toubiz/Legacy/Db');
        if ($configuration) {
            foreach ($configuration['languages'] as $language) {
                $this->synchronizePointOfInterestsFromDbService($configuration, $language, $limit);
            }
        }
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypeGastronomy(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Toubiz/Legacy/GastronomyApi');
        if ($configuration) {
            foreach ($configuration['languages'] as $language) {
                $this->synchronizeGastronomyFromApi($configuration, $language, $limit);
            }
        }
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypeDirectMarketers(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Toubiz/Legacy/DirectMarketerApi');
        if ($configuration) {
            foreach ($configuration['languages'] as $language) {
                $this->synchronizeDirectMarketersFromApi($configuration, $language, $limit);
            }
        }
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypeTours(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Outdooractive/Api');
        if ($configuration) {
            foreach ($configuration['languages'] as $language) {
                $this->synchronizeToursFromApi($configuration, $language, $limit);
            }
        }
    }

    /** @noinspection PhpUnusedPrivateMethodInspection */
    private function synchronizeArticleTypeCongressLocations(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Tportal/CongressApi');
        if ($this->hasClients($configuration)) {
            foreach ($this->getClients($configuration) as $clientConfiguration) {
                foreach ($clientConfiguration['languages'] ?? [ 'de' ] as $language) {
                    $this->synchronizeCongressLocationsFromTportal($clientConfiguration, $language, $limit);
                }
            }
        }
    }

    protected function synchronizeArticleTypeCities(int $limit = null): void
    {
        $configuration = $this->getConfigurationForService('Toubiz/Legacy/CityApi');
        if ($configuration) {
            foreach ($configuration['languages'] as $language) {
                $this->synchronizeCitiesFromApi($configuration, $language, $limit);
            }
        }
    }

    private function importGeometry()
    {
        if ($this->geometryRepository->isEmpty()) {
            $this->outputLine('Geometries not found, running geometry:seedzippolygons command');
            Scripts::executeCommand('newland.toubiz.sync.neos:geometry:seedzippolygons', $this->flowSettings);

            $this->outputLine("\nGeometries Imported, Continuing...");
        } else {
            $this->outputLine("\nGeometries Found, Continuing...");
        }
    }

    private function flushObjectPathMappings()
    {
        $this->objectPathMappingService->flushMappings(Article::class);
        $this->emitFlush(static::TYPE_FLUSH);
    }
}
