<?php declare(strict_types=1);
namespace Newland\Toubiz\Sync\Neos\Importer;

/*
 * 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\Common\Collections\Collection;
use Neos\Flow\Annotations as Flow;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleTypeCityAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\AwardAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\HasLocationDataInterface;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithRelatedLists;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithSocialMediaLinksAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithStarRatings;
use Newland\Toubiz\Api\ObjectAdapter\ArticleAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\ExternalIdAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\FileAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\HasAdditionalExternalIds;
use Newland\Toubiz\Api\ObjectAdapter\HasAwards;
use Newland\Toubiz\Api\ObjectAdapter\HasKitchenTimes;
use Newland\Toubiz\Api\ObjectAdapter\HasLanguageGroupingSeparateFromOriginalId;
use Newland\Toubiz\Api\ObjectAdapter\HasMarketingPriceInterface;
use Newland\Toubiz\Api\ObjectAdapter\HasSeoInformationInterface;
use Newland\Toubiz\Api\ObjectAdapter\KitchenTimeAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\MediumAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\ProvidesRawJson;
use Newland\Toubiz\Api\ObjectAdapter\UriAdapterInterface;
use Newland\Toubiz\Api\Service\WithUuidPredictionService;
use Newland\Toubiz\Api\Service\WithUuidPredictionServiceInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\Award;
use Newland\Toubiz\Sync\Neos\Domain\Model\Category;
use Newland\Toubiz\Sync\Neos\Domain\Model\CityData;
use Newland\Toubiz\Sync\Neos\Domain\Model\Medium;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\ArticleList;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\RelatedLists;
use Newland\Toubiz\Sync\Neos\Domain\Model\ZipCode;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Domain\Repository\RecordConfigurationRepository;
use Newland\Toubiz\Sync\Neos\Enum\ArticleType;

class ArticleImporter extends AbstractImporter
{
    use MapsMarketingPrice;
    use MapsSeoInformation;
    use ClientAware;
    use WithUuidPredictionService;

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

    /**
     * @var RecordConfigurationRepository
     * @Flow\Inject()
     */
    protected $recordConfiguration;

    /**
     * @param ArticleAdapterInterface $data
     * @return void
     */
    public function remove(ArticleAdapterInterface $data): void
    {
        $article = $this->findArticle($data);

        if ($article !== null) {
            $this->articleRepository->remove($article);
            $this->persistenceManager->persistAll();
        }
    }

    /**
     * Import method.
     *
     * Persist given data by creating new objects or updating existing ones.
     *
     * @param ArticleAdapterInterface $data
     */
    public function import($data): ?Article
    {
        $this->initializeLogger(
            $data,
            [
                'article' => [
                    'type' => ArticleType::$map[$data->getMainType()] ?? $data->getMainType(),
                    'externalId' => $data->getExternalId(),
                    'name' => $data->getName(),
                ],
            ]
        );

        $article = $this->findArticle($data);
        $persisted = $article !== null;

        if ($article === null) {
            $article = new Article();
        }

        $this->mapSimpleValues($data, $article);
        $this->mapRatingValues($data, $article);
        $article->setClient($this->client);
        $this->mapOpeningTimes($data, $article);

        $this->mapGeoCoordinates($data, $article);
        $this->mapAddresses($data, $article);
        $this->mapMainAddress($data, $article);
        $this->mapMainCategory($data, $article);
        $this->mapCategories($data, $article);
        $this->mapFiles($data, $article);
        $this->mapMedia($data, $article);
        $this->mapAttributes($data, $article);
        $this->mapBookingUris($data, $article);
        $this->updateRandomSorting($article);

        if ($data instanceof HasMarketingPriceInterface) {
            $article->setMarketingPrice($this->mapMarketingPrice($data));
        }

        if ($data instanceof HasAdditionalExternalIds) {
            $this->mapExternalIds($article, $data);
        }

        if ($data instanceof ArticleWithSocialMediaLinksAdapterInterface) {
            $this->mapSocialMediaLinks($data, $article);
        }

        if ($data instanceof ArticleTypeCityAdapterInterface) {
            $this->mapCityData($data, $article);
        }

        if ($data instanceof ArticleWithRelatedLists) {
            $this->mapRelatedLists($data, $article);
        }

        if ($data instanceof HasLocationDataInterface) {
            $this->mapLocationData($data, $article);
        }

        if ($data instanceof ProvidesRawJson) {
            $article->setRawJson($data->getRawJson());
        }

        if ($data instanceof HasLanguageGroupingSeparateFromOriginalId) {
            $article->setLanguageGrouping($data->getLanguageGrouping() ?: $data->getExternalId());
        } else {
            $article->setLanguageGrouping($data->getExternalId());
        }

        if ($data instanceof HasKitchenTimes) {
            $this->mapKitchenTimes($data, $article);
        }

        if ($data instanceof HasAwards) {
            $this->mapAwards($data, $article);
        }

        if ($data instanceof HasSeoInformationInterface) {
            $this->mapSeoInformation($data->getSeoInformation(), $article);
        }

        $article->setUpdatedAt(new \DateTime);
        $article->setLastModifiedAt($data->getLastModifiedAt());

        if ($persisted) {
            $this->articleRepository->update($article);
        } else {
            $this->articleRepository->add($article);
        }

        $this->recordConfiguration->applyRecordConfigurationToRecord($article, false);
        return $article;
    }

    protected function mapExternalIds(Article $entity, HasAdditionalExternalIds $adapter): void
    {
        $importer = new ExternalIdImporter();
        $importer->loggingContext($this->context);
        $this->updateCollection(
            $entity->getExternalIds(),
            $adapter->getAdditionalExternalIds(),
            function (ExternalIdAdapterInterface $adapter) use ($importer) {
                return $importer->import($adapter);
            }
        );
    }

    protected function mapSocialMediaLinks(ArticleWithSocialMediaLinksAdapterInterface $data, Article $article): void
    {
        $article->setFacebookUri($data->getFacebookUri());
        $article->setTwitterUri($data->getTwitterUri());
        $article->setInstagramUri($data->getInstagramUri());
        $article->setYoutubeUri($data->getYoutubeUri());
        $article->setWikipediaUri($data->getWikipediaUri());
        $article->setFlickrUri($data->getFlickrUri());
    }


    private function findArticle(ArticleAdapterInterface $data): ?Article
    {
        return $this->articleRepository->withLanguage(
            $data->getLanguage(),
            function () use ($data) {
                return $this->articleRepository->findOneByOriginalIdAndClient(
                    $data->getExternalId(),
                    $this->client
                );
            }
        );
    }

    protected function mapSimpleValues(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setOriginalId($data->getExternalId());
        $article->setMainType($data->getMainType());
        $article->setName($data->getName());
        $article->setProcessedName($data->getName());
        $article->setAbstract($data->getAbstract());
        $article->setDescription($data->getDescription());
        $article->setDetailUri($data->getDetailUri());
        $article->setLanguage($data->getLanguage());
        $article->setUrlIdentifier($this->generateUrlIdentifierFromData($data));
        $article->setSourceSystem($data->getSourceSystem());
        $article->setSourceApiVersion($data->getSourceApiVersion());
        $article->setAdditionalSearchString($data->getAdditionalSearchString());
        $article->setCanonicalUrl($data->getCanonicalUrl());
    }

    protected function mapBookingUris(ArticleAdapterInterface $data, Article $article): void
    {
        $this->updateCollection(
            $article->getBookingUris(),
            $data->getBookingUris(),
            function (UriAdapterInterface $adapter) use ($article) {
                $uri = (new UriImporter())->import($adapter);
                $uri->setArticleAsBookingUri($article);
                return $uri;
            }
        );
    }

    protected function mapRatingValues(ArticleAdapterInterface $data, Article $article): void
    {
        if ($data instanceof ArticleWithStarRatings) {
            $this->updateCollection(
                $article->getStarClassifications(),
                $data->getStarRatings(),
                function ($adapter) {
                    $importer = new StarClassificationImporter();
                    $importer->loggingContext($this->context);
                    $importer->setLanguage($this->language);
                    return $importer->import($adapter);
                }
            );
        }

        $article->setAverageRating($data->getAverageRating() ?: 0);
        $article->setNumberOfRatings($data->getNumberOfRatings() ?: 0);
    }

    protected function mapGeoCoordinates(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setLatitude($data->getLatitude());
        $article->setLongitude($data->getLongitude());
    }

    protected function mapAddresses(ArticleAdapterInterface $data, Article $article): void
    {
        $this->updateCollection(
            $article->getAddresses(),
            $data->getAddresses(),
            function ($adapter) {
                $addressImporter = new AddressImporter();
                $addressImporter->setLanguage($this->language);
                $addressImporter->loggingContext($this->context);
                return $addressImporter->import($adapter);
            }
        );
    }

    protected function mapMainAddress(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setMainAddress(null);

        $mainAddress = null;
        $mainAddressAdapter = $data->getMainAddress();

        if ($mainAddressAdapter) {
            $addressImporter = new AddressImporter();
            $addressImporter->setLanguage($this->language);
            $addressImporter->loggingContext($this->context);
            $mainAddress = $addressImporter->import($mainAddressAdapter);
        }

        $article->setMainAddress($mainAddress);
    }

    protected function mapMainCategory(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setMainCategory(null);

        $category = null;
        $categoryAdapter = $data->getMainCategory();

        if ($categoryAdapter) {
            $importer = new CategoryImporter();
            $importer->loggingContext($this->context);
            $importer->setLanguage($data->getLanguage());
            $category = $importer->import($categoryAdapter);
            $this->persistenceManager->persistAll();
        }

        $article->setMainCategory($category);
    }

    protected function mapCategories(ArticleAdapterInterface $data, Article $article): void
    {
        $importer = new CategoryImporter();
        $importer->loggingContext($this->context);
        $importer->setLanguage($data->getLanguage());
        [ $categories ] = $this->updateCollection(
            $article->getCategories(),
            $data->getCategories(),
            function ($adapter) use ($importer) {
                return $importer->import($adapter);
            }
        );

        $sorting = array_map(
            static function (Category $category) {
                return $category->getOriginalId();
            },
            $categories->toArray()
        );

        $article->setCategorySorting($sorting);
    }

    protected function mapMedia(ArticleAdapterInterface $data, Article $article): void
    {
        $existing = [];
        foreach ($article->getMedia() as $media) {
            $existing[$media->getOriginalId()] = $media;
        }

        $importer = new MediumImporter();
        $importer->loggingContext($this->context);
        $articleType = ArticleType::$map[$data->getMainType()] ?? $data->getMainType();
        $importer->setDownload($this->packageConfig['downloadImages'][$articleType] ?? false);

        /** @var Collection<int, Medium> $media */
        [ $media ] = $this->updateCollection(
            $article->getMedia(),
            $data->getMedia(),
            function (MediumAdapterInterface $adapter) use ($existing, $importer) {
                return $importer->import($adapter, $existing[$adapter->getExternalId()] ?? null);
            }
        );

        $sorting = array_map(
            function (Medium $medium) {
                return $medium->getOriginalId();
            },
            $media->toArray()
        );
        $article->setRelationSortingForField('media', $sorting);

        $mainMediumEntry = $data->getMainMedium();
        $mainMedium = null;
        if ($mainMediumEntry) {
            $existingMainMedium = null;
            foreach ($media as $medium) {
                if ($medium->getOriginalId() === $mainMediumEntry->getExternalId()) {
                    $existingMainMedium = $medium;
                    break;
                }
            }

            $mainMedium = $importer->import($mainMediumEntry, $existingMainMedium);
        }
        $article->setMainMedium($mainMedium);
    }

    protected function mapFiles(ArticleAdapterInterface $data, Article $article): void
    {
        $existing = [];
        foreach ($article->getFiles() as $file) {
            $existing[$file->getOriginalId()] = $file;
        }

        $this->updateCollection(
            $article->getFiles(),
            $data->getFiles(),
            function (FileAdapterInterface $adapter) use ($existing) {
                return (new FileImporter)
                    ->import($adapter, $existing[$adapter->getExternalId()] ?? null);
            }
        );
    }

    public function mapOpeningTimes(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setOpeningTimesFormat($data->getOpeningTimesFormat());
        $article->setOpeningTimes($data->getOpeningTimes());
    }

    protected function mapAttributes(ArticleAdapterInterface $data, Article $article): void
    {
        $attributes = $article->getAttributes();
        $attributes->clear();

        $attributesData = $data->getAttributes();
        if ($attributesData) {
            foreach ($attributesData as $attributeData) {
                $importer = new AttributeImporter();
                $importer->loggingContext($this->context);
                $importer->setArticle($article);
                $attribute = $importer->import($attributeData);
                if (!$attributes->contains($attribute)) {
                    $attributes->add($attribute);
                }
            }
        }

        $article->setAttributes($attributes);
    }

    protected function mapRelatedLists(ArticleWithRelatedLists $data, Article $article): void
    {
        if ($data instanceof WithUuidPredictionServiceInterface) {
            $data->setUuidPredictionService($this->uuidPredictionService);
        }

        $relatedListsData = $data->getRelatedLists();

        if ($relatedListsData === null) {
            return;
        }

        $relatedLists = new RelatedLists();
        $poiHighlightsData = $relatedListsData->getHighlightsList();
        if ($poiHighlightsData) {
            $poiHighlightsList = new ArticleList(
                $poiHighlightsData->getTitle(),
                $poiHighlightsData->getArticleIdentifiers()
            );
            $relatedLists->setPoiHighlights($poiHighlightsList);
        }

        $articleLists = [];
        foreach ($relatedListsData->getArticleLists() as $listData) {
            $articleLists[] = new ArticleList(
                $listData->getTitle(),
                $listData->getArticleIdentifiers(),
                $listData->getCategoryIdentifiers()
            );
        }

        $relatedLists->setArticleLists($articleLists);
        $article->setRelatedLists($relatedLists);
    }

    private function mapCityData(ArticleTypeCityAdapterInterface $data, Article $article): void
    {
        $cityData = $article->getCityData() ?? new CityData();
        $cityData->setCity($article);
        $cityData->setIdToubiz($data->getIdToubiz());
        $cityData->setIdTomas($data->getIdTomas());

        $zipCodeImporter = new ZipCodeImporter();
        $zipCodeImporter->setUuidPredictionService($this->uuidPredictionService);
        $this->updateCollection(
            $cityData->getZipCodes(),
            $data->getZipCodes(),
            function ($zipCode) use ($zipCodeImporter) {
                if (preg_match(ZipCode::VALID_PATTERN, $zipCode)) {
                    return $zipCodeImporter->import($zipCode);
                }
                return null;
            }
        );

        $cityData->setClaim($data->getClaim());
        $cityData->setFacts($data->getFacts());
        $cityData->setWebcamUrl($data->getWebcamUrl());
        $cityData->setWebcamDescription($data->getWebcamDescription());
        $cityData->setNews($data->getNews());

        $article->setCityData($cityData);
    }

    private function generateUrlIdentifierFromData(ArticleAdapterInterface $data): string
    {
        return implode(
            '-',
            [
                $data->getExternalId(),
                $this->client,
                $data->getMainType(),
                $data->getLanguage(),
            ]
        );
    }

    private function mapLocationData(HasLocationDataInterface $data, Article $article): void
    {
        $language = $this->language ?: $article->getLanguage();

        $cities = [ [] ];
        $this->articleRepository->withLanguage(
            $language,
            function () use ($data, &$cities) {
                foreach ($data->getCitySelectors() as $selector) {
                    $cities[] = $this->articleRepository->findByExternalIdSelector($selector);
                }
            }
        );

        $this->setCollectionValues($article->getCities(), array_merge(...$cities));
    }

    private function updateRandomSorting(Article $article): void
    {
        // Using 4 bytes of length as default range as it is the lowest of the commonly used databases.
        // * SQLite: -(2^63) - (2^63)-1
        // * MySQL: -(2^31) - (2^31)-1
        // * PostgreSQL: -(2^31) - (2^31)-1
        $range = 2 ** 32;
        $min = -1 * (($range / 2) - 1);
        $max = ($range / 2) - 1;
        $article->setRandomSorting(random_int($min, $max));
    }

    private function mapKitchenTimes(HasKitchenTimes $adapter, Article $article): void
    {
        $importer = new KitchenTimeImporter();
        $importer->loggingContext($this->context);

        $this->updateCollection(
            $article->getKitchenTimes(),
            $adapter->getKitchenTimes(),
            function (KitchenTimeAdapterInterface $kitchenTimeAdapter) use ($importer, $article) {
                $kitchenTime = $importer->import($kitchenTimeAdapter);
                if ($kitchenTime) {
                    $kitchenTime->setArticle($article);
                }
                return $kitchenTime;
            }
        );
    }

    private function mapAwards(HasAwards $adapter, Article $article): void
    {
        $importedAwardIds = array_map(
            function (AwardAdapterInterface $awardAdapter) use ($article) {
                $importer = new AwardImporter();
                $importer->loggingContext($this->context);
                $importer->setLanguage($this->language);

                $award = $importer->import($awardAdapter);
                if ($award) {
                    $award->setArticle($article);
                }
                return $award->getPersistenceObjectIdentifier();
            },
            $adapter->getAwards()
        );

        $persistedAwards = $article->getAwards();
        /** @var Award $award */
        foreach ($persistedAwards as $award) {
            if (in_array($award->getPersistenceObjectIdentifier(), $importedAwardIds, true) === false) {
                $persistedAwards->removeElement($award);
            }
        }
    }
}
