<?php

declare(strict_types=1);

namespace External\FlexiBeeBundle\Synchronizers;

use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Util\FlexiBeeApi;
use External\FlexiBeeBundle\Util\FlexiBeeConfiguration;
use External\FlexiBeeBundle\Util\FlexiBeeLogger;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;

abstract class BaseSynchronizer implements SynchronizerInterface
{
    public const MODE_NORMAL = 0;
    public const MODE_FULL = 1;

    protected static string $type;
    protected static ?string $evidenceClass = null;

    protected bool $logging = true;
    protected int $mode = self::MODE_NORMAL;

    protected FlexiBeeLogger $logger;
    protected FlexiBeeConfiguration $configuration;
    protected FlexiBeeApi $flexiBeeApi;
    protected FlexiBeeUtil $flexiBeeUtil;

    protected ?int $tmpLastSync = null;

    public function __construct(
        FlexiBeeConfiguration $configuration,
        FlexiBeeApi $flexiBeeApi,
        FlexiBeeUtil $flexiBeeUtil,
        FlexiBeeLogger $logger,
    ) {
        $this->configuration = $configuration;
        $this->flexiBeeApi = $flexiBeeApi;
        $this->flexiBeeUtil = $flexiBeeUtil;
        $this->logger = $logger;
    }

    public static function getType(): string
    {
        return static::$type;
    }

    public function sync(): void
    {
        $this->process(
            $this->getLastSyncTime()
        );

        $this->updateLastSyncTime();
    }

    public function syncSingleItem(int $id, ?int $flexiId = null): void
    {
        throw new FlexiBeeException(sprintf('Single item synchronization is not supported for type "%s"', static::getType()));
    }

    protected function process(?int $lastSyncTime = null): void
    {
        foreach ($this->getItems($lastSyncTime) as $item) {
            $this->logItem((array) $item);
            $this->processItem($item);
        }
    }

    abstract protected function processItem(array $item): void;

    protected function getItems(?int $lastSyncTime = null): iterable
    {
        if (!static::$evidenceClass) {
            throw new FlexiBeeException(
                sprintf('Class "%s" should overwrite "%s" method', get_class($this), __FUNCTION__)
            );
        }

        // Pokud je nastavena evidence class, tak pro nacteni zmen pouziju ChangesAPI

        $changesMaxVersion = $this->getLastVersion();

        // ukladam si startTime, abych mohl limitovat maximalni dobu, kterou muze sync v ramci jednoho sync cyklu bezet
        $startTime = microtime(true);

        do {
            // nactu si zmeny od posledni synchronizace
            $changes = $this->flexiBeeApi->getChanges($changesMaxVersion, static::$evidenceClass);
            // ulozim si maximalni verzi
            $changesMaxVersion = (int) max($changesMaxVersion, !empty($changes) ? (max(array_map(fn ($x) => $x['@in-version'], $changes)) + 1) : 0);
            // nactu si data podle ID zmen
            $changesIds = array_unique(array_map(fn ($x) => $x['id'], $changes));
            if (!empty($changesIds)) {
                // zavolam preprocess kvuli pripadnym modifikacim dat
                $changedItems = $this->preprocessChangedItems(
                    $this->flexiBeeApi->getEvidenceDataByIds(static::$evidenceClass, $changesIds, filters: $this->getItemsFilter())
                );
                // zacnu vracet polozky, aby se v synchronizaci zpracovali
                foreach ($changedItems as $item) {
                    yield $item;
                }
            }

            // aktualizuju si posledni sesynchronizovanou verzi
            $this->updateLastVersion($changesMaxVersion);

            $isTimedOut = (microtime(true) - $startTime) > 300;
            // pokud sync bezi uz dele jak 5 minut, tak ji ukoncim
            // pravdepodobne ve Flexi vzniklo hodne zmen, tak to postupne syncneme nez blokovat sync na dlouhou dobu
            if ($this->mode === self::MODE_NORMAL && $isTimedOut) {
                break;
            }
        } while (!empty($changes));
    }

    protected function preprocessChangedItems(array $items): array
    {
        return $items;
    }

    protected function logItem(array $item): void
    {
        if (!$this->logging) {
            return;
        }

        $this->logger->data(
            sprintf('[FlexiBee] Processing change of \'%s\'', static::getType()),
            [
                'Data' => $item,
                'Type' => static::getType(),
            ]
        );
    }

    protected function logUpdate(array $item): void
    {
        if (!$this->logging) {
            return;
        }

        $this->logger->data(
            sprintf('[FlexiBee] Sending to Flexi change of \'%s\'', static::getType()),
            [
                'Data' => $item,
                'Type' => static::getType(),
            ]
        );
    }

    protected function getLastVersion(?string $type = null): int
    {
        if ($this->mode === self::MODE_FULL) {
            return 0;
        }

        $dbcfg = \Settings::getDefault();
        $flexiBee = $dbcfg->loadValue('flexibee') ?: [];

        return !empty($flexiBee['sync_versions'][$type ?: static::getType()]) ? (int) $flexiBee['sync_versions'][$type ?: static::getType()] : 0;
    }

    protected function updateLastVersion(int $version): void
    {
        if ($this->mode === self::MODE_FULL) {
            return;
        }

        $dbcfg = \Settings::getDefault();
        $flexiBee = $dbcfg->loadValue('flexibee') ?: [];

        $flexiBee['sync_versions'][static::getType()] = $version;

        $dbcfg->saveValue('flexibee', $flexiBee);
    }

    protected function getLastSyncTime(?string $type = null): ?int
    {
        if ($this->mode === self::MODE_FULL) {
            return null;
        }

        $dbcfg = \Settings::getDefault();
        $flexiBee = $dbcfg->loadValue('flexibee') ?: [];

        $this->tmpLastSync = time() - (60 * 5);

        return $flexiBee['timestamps'][$type ?: static::getType()] ?? null;
    }

    protected function updateLastSyncTime(): void
    {
        if ($this->mode === self::MODE_FULL) {
            return;
        }

        $dbcfg = \Settings::getDefault();
        $flexiBee = $dbcfg->loadValue('flexibee') ?: [];

        $flexiBee['timestamps'][static::getType()] = $this->tmpLastSync ?: (time() - (60 * 5));

        $dbcfg->saveValue('flexibee', $flexiBee);
    }

    protected function createDateTime(?int $timestamp): ?\DateTime
    {
        if (!$timestamp) {
            return null;
        }

        $datetime = new \DateTime();
        $datetime->setTimestamp($timestamp);

        return $datetime;
    }

    public function setMode(int $mode): self
    {
        $this->mode = $mode;

        return $this;
    }

    protected function getItemsFilter(): array
    {
        return [];
    }
}
