<?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 Newland\Toubiz\Api\ObjectAdapter\ArticleAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Api\Service\Outdooractive;
use Newland\Toubiz\Api\Service\ServiceFactory;
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\Importer\ArticleImporter;

/**
 * Articles command controller.
 *
 * Provides commands to manipulate article data.
 *
 * @Flow\Scope("singleton")
 */
class ArticlesCommandController extends AbstractCommandController
{
    const TYPE_LODGINGS = 'lodgings';
    const TYPE_POI = 'poi';
    const TYPE_GASTRONOMY = 'gastronomy';
    const TYPE_DIRECT_MARKETERS = 'directMarketers';
    const TYPE_TOURS = 'tours';

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

    /**
     * Synchronize command.
     *
     * Updates local articles database from API data source.
     *
     * @param bool $quiet
     * @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.
     * @return void
     */
    public function synchronizeCommand($quiet = false, int $minutesToLive = 10080)
    {
        if (!$quiet) {
            $this->showProgressOnCommandLine();
        }

        $this->synchronizeArticles();
        $this->deleteOldRecords($minutesToLive);
        $this->objectPathMappingService->flushMappings(Article::class);
    }

    /**
     * Synchronize tours command.
     *
     * Updates local database with tour articles from API data source.
     *
     * @param bool $quiet
     * @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 null|int $limit
     * @return void
     * @throws \Exception
     */
    public function synchronizeToursCommand($quiet = false, $minutesToLive = 10080, $limit = null)
    {
        if (!$quiet) {
            $this->showProgressOnCommandLine();
        }

        $configuration = $this->getConfigurationForService('Outdooractive/Api');
        if ($configuration) {
            $this->synchronizeToursFromApi($configuration, $limit);
        }

        $this->deleteOldRecords($minutesToLive);
    }

    /**
     * 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.
     * @throws \Neos\Flow\Persistence\Exception\IllegalObjectTypeException
     */
    public function removeCommand(string $where = null)
    {
        $query = $this->articleRepository->createQueryBuilder('article');
        if ($where) {
            $query->where($where);
            $this->outputLine('Deleting articles WHERE ' . $where);
        } else {
            $this->outputLine('Deleting all articles');
        }

        $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->output->progressStart($count);

        foreach ($query->getQuery()->execute() as $article) {
            $this->articleRepository->remove($article);
            $this->output->progressAdvance();
        }

        $this->output->progressFinish();
    }

    /**
     * Synchronizes lodging data as articles from TPortal.
     *
     * @param array $configuration
     * @return void
     */
    protected function synchronizeLodgingsFromTportal($configuration)
    {
        $this->emitStart(static::TYPE_LODGINGS);
        $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);


        $processed = 0;
        $service->fetch(
            'lodgings',
            $this->handleImportExceptions(function ($record) use ($client, &$processed) {
                $this->emitProgress(static::TYPE_LODGINGS, ++$processed);
                $importer = new ArticleImporter;
                $importer->setClient($client);
                $importer->import($record);
            })
        );

        $this->emitEnd(static::TYPE_LODGINGS);
    }

    /**
     * Synchronizes POI data from legacy toubiz DB service.
     *
     * @param array $configuration
     * @return void
     */
    protected function synchronizePointOfInterestsFromDbService($configuration)
    {
        $this->emitStart(static::TYPE_POI);

        $service = ServiceFactory::get('Toubiz/Legacy/Db', $configuration['baseUri'] ?? null);
        $service->setClientName($configuration['client']);
        $service->setApiKey($configuration['apiKey']);

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

        $processed = 0;
        $service->fetch(
            'pointOfInterests',
            $this->handleImportExceptions(
                function (PointOfInterestAdapter $record) use ($minLevelOfMaintenance, &$processed) {
                    $this->emitProgress(static::TYPE_POI, ++$processed);
                    if ($record->getLevelOfMaintenance() > $minLevelOfMaintenance) {
                        (new ArticleImporter())->import($record);
                    } else {
                        (new ArticleImporter())->remove($record);
                    }
                }
            )
        );

        $this->emitEnd(static::TYPE_POI);
    }

    /**
     * Synchronizes gastronomy data from legacy API.
     *
     * @param array $configuration
     * @return void
     */
    protected function synchronizeGastronomyFromApi($configuration)
    {
        $this->emitStart(static::TYPE_GASTRONOMY);

        $service = ServiceFactory::get('Toubiz/Legacy/GastronomyApi', $configuration['baseUri'] ?? null);
        $service->setApiKey($configuration['apiKey']);

        $processed = 0;
        $service->fetch(
            'gastronomy',
            $this->handleImportExceptions(function (GastronomyAdapter $record) use (&$processed) {
                $this->emitProgress(static::TYPE_GASTRONOMY, ++$processed);
                if ($record->getOnlineStatus()) {
                    (new ArticleImporter())->import($record);
                } else {
                    (new ArticleImporter())->remove($record);
                }
            })
        );

        $this->emitEnd(static::TYPE_GASTRONOMY);
    }

    /**
     * Synchronizes direct marketers from toubiz legacy API.
     *
     * @param array $configuration
     * @return void
     */
    protected function synchronizeDirectMarketersFromApi(array $configuration)
    {
        $this->emitStart(static::TYPE_DIRECT_MARKETERS);

        $service = ServiceFactory::get('Toubiz/Legacy/DirectMarketerApi', $configuration['baseUri'] ?? null);
        $service->setApiKey($configuration['apiKey']);

        $processed = 0;
        $service->fetch(
            'directMarketers',
            $this->handleImportExceptions(function (DirectMarketerAdapter $record) use (&$processed) {
                $this->emitProgress(static::TYPE_DIRECT_MARKETERS, ++$processed);
                $importer = new ArticleImporter;
                if ($record->getOnlineStatus()) {
                    $importer->setOpeningTimesFormat(ArticleConstants::OPENING_TIMES_FORMAT_LEGACY);
                    $importer->import($record);
                } else {
                    $importer->remove($record);
                }
            })
        );

        $this->emitEnd(static::TYPE_DIRECT_MARKETERS);
    }

    /**
     * Synchronizes tours.
     *
     * @param array $configuration
     * @param int|null $limit
     * @return void
     * @throws \Exception
     */
    protected function synchronizeToursFromApi($configuration, $limit = null)
    {
        $this->emitStart(static::TYPE_TOURS);

        /** @var Outdooractive\ApiService $service */
        $service = ServiceFactory::get('Outdooractive/Api', $configuration['baseUri'] ?? null);
        $service->setClientName($configuration['client']);
        $service->setApiKey($configuration['apiKey']);

        $records = $service->fetchTours();
        if (!$records) {
            throw new \Exception('No tour data received for synchronizing!');
        }

        $limit = $limit ?? \count($records);

        for ($i = 0; $i < $limit; $i++) {
            $this->emitProgress(static::TYPE_TOURS, $i);
            $this->handleImportExceptions(function (ArticleAdapterInterface $article) {
                (new ArticleImporter())->import($article);
            })($records[$i]);
        }

        $this->emitEnd(static::TYPE_TOURS);
    }

    private function synchronizeArticles()
    {
        $configuration = $this->getConfigurationForService('Tportal/Api');
        if ($this->hasClients($configuration)) {
            foreach ($this->getClients($configuration) as $clientConfiguration) {
                $this->synchronizeLodgingsFromTportal($clientConfiguration);
            }
        }

        $configuration = $this->getConfigurationForService('Toubiz/Legacy/Db');
        if ($configuration) {
            $this->synchronizePointOfInterestsFromDbService($configuration);
        }

        $configuration = $this->getConfigurationForService('Toubiz/Legacy/GastronomyApi');
        if ($configuration) {
            $this->synchronizeGastronomyFromApi($configuration);
        }

        $configuration = $this->getConfigurationForService('Toubiz/Legacy/DirectMarketerApi');
        if ($configuration) {
            $this->synchronizeDirectMarketersFromApi($configuration);
        }

        $configuration = $this->getConfigurationForService('Outdooractive/Api');
        if ($configuration) {
            $this->synchronizeToursFromApi($configuration);
        }
    }

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