<?php
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 Neos\Flow\Annotations as Flow;
use Neos\Flow\ResourceManagement\Exception as ResourceManagementException;
use Neos\Flow\ResourceManagement\PersistentResource;
use Neos\Flow\ResourceManagement\ResourceManager;
use Newland\Toubiz\Api\ObjectAdapter\Concern\MediumConstants;
use Newland\Toubiz\Api\ObjectAdapter\MediumAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\MediumWithGeneratedPreviewUri;
use Newland\Toubiz\Api\ObjectAdapter\MediumWithStaticPreviewUri;
use Newland\Toubiz\Sync\Neos\Domain\Model\Medium;
use Newland\Toubiz\Sync\Neos\Domain\Repository\MediumRepository;

class MediumImporter extends AbstractImporter
{
    /**
     * @Flow\Inject()
     * @var MediumRepository
     */
    protected $mediumRepository;

    /**
     * @var ResourceManager
     * @Flow\Inject()
     */
    protected $resourceManager;

    /** @var bool */
    protected $download = false;

    public function setDownload(bool $download): void
    {
        $this->download = $download;
    }

    /**
     * Import method.
     *
     * Persist given data by creating new objects or updating existing ones.
     *
     * @param MediumAdapterInterface $data
     */
    public function import($data, Medium $medium = null): ?Medium
    {
        $this->initializeLogger($data, [
            'medium' => [
                'externalId' => $data->getExternalId(),
                'title' => $data->getTitle(),
                'sourceUri' => $data->getSourceUri(),
            ]
        ]);

        $persisted = (bool) $medium;

        if (!$medium) {
            $medium = new Medium();
        }
        $sourceUriBefore = $medium->getSourceUri();

        $medium->setOriginalId($data->getExternalId());
        $medium->setTitle($data->getTitle());
        $medium->setType($data->getType());
        $medium->setCopyright($data->getCopyright());
        $medium->setAltText((string) ($data->getAltText() ?: $data->getTitle()));
        $medium->setDescription((string) ($data->getDescription() ?: $data->getAltText() ?: $data->getTitle()));

        $mapped = $this->mapImage($medium, $data);
        if (!$mapped) {
            return null;
        }

        $medium->setPreviewUri($this->getPreviewUri($medium, $data, $sourceUriBefore));

        if ($persisted) {
            $this->mediumRepository->update($medium);
        } else {
            $this->mediumRepository->add($medium);
        }

        return $medium;
    }

    private function mapImage(Medium $medium, MediumAdapterInterface $data): bool
    {
        $resourceDoesNotExist = $medium->resourceExistsOnDisk() === false;
        $resourceHasChanged = $data->getSourceUri() !== $medium->getSourceUri();

        if ($this->download
            && ($resourceDoesNotExist || $resourceHasChanged)
            && $data->getType() === MediumConstants::TYPE_IMAGE) {
            $this->downloadRetries = 0;
            $resource = $this->downloadAsResource($data);
            if ($resource === null) {
                return false;
            }
            $medium->setResource($resource);
        }

        $medium->setSourceUri($data->getSourceUri());
        return true;
    }

    /** @var int */
    private $downloadRetries = 0;
    private function downloadAsResource(MediumAdapterInterface $data): ?PersistentResource
    {
        try {
            $resource = $this->resourceManager->importResource($data->getSourceUri());
            if ($this->downloadRetries > 0) {
                $this->debug(sprintf('Successful download after %d retries', $this->downloadRetries));
            }
            return $resource;
        } catch (ResourceManagementException $e) {
            if (++$this->downloadRetries > 10) {
                $this->logResourceManagementException($data, $e);
                return null;
            }

            $sleep = $this->exponentialBackoffTime($this->downloadRetries);
            $this->debug(sprintf(
                'Could not download %s. Retrying again in %fs',
                $data->getSourceUri(),
                $sleep / 1000000
            ));
            usleep($sleep);
            return $this->downloadAsResource($data);
        }
    }

    private function getPreviewUri(Medium $medium, MediumAdapterInterface $data, string $sourceUriBefore): ?string
    {
        if ($data instanceof MediumWithStaticPreviewUri) {
            return $data->getPreviewUri();
        }

        if ($data instanceof MediumWithGeneratedPreviewUri) {
            // Only generate preview uri if source uri has changed or has not been set before.
            if ($sourceUriBefore !== $medium->getSourceUri() || empty(trim((string) $medium->getPreviewUri()))) {
                return $data->generatePreviewUri();
            }

            return $medium->getPreviewUri();
        }

        return null;
    }

    private function logResourceManagementException(
        MediumAdapterInterface $data,
        ResourceManagementException $exception
    ): void {
        $message = sprintf(
            'Could not download media element with external id `%s` from `%s` (Tried %d times). Ignoring this image.',
            $data->getExternalId(),
            $data->getSourceUri(),
            $this->downloadRetries
        );
        $this->warning($message);
    }

    /**
     * Implements simple exponential backoff with some randomness:
     * - First retry will be between 100ms - 200ms
     * - Second retry will be between 200ms - 400ms
     * - Third retry will be between 400ms - 800ms
     * - ...
     */
    private function exponentialBackoffTime(int $retry): int
    {
        $multiplier = $retry ^ 2;
        $minMicro = 100000 * $multiplier;
        $maxMicro = 200000 * $multiplier;
        return random_int($minMicro, $maxMicro);
    }
}
