<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\DocGeneratorBundle\Service\Generator;

use Chill\DocGeneratorBundle\Context\ContextManagerInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface;
use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Exception\StoredObjectManagerException;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\Yaml\Yaml;

class Generator implements GeneratorInterface
{
    private const LOG_PREFIX = '[docgen generator] ';

    public function __construct(
        private readonly ContextManagerInterface $contextManager,
        private readonly DriverInterface $driver,
        private readonly ManagerRegistry $objectManagerRegistry,
        private readonly LoggerInterface $logger,
        private readonly StoredObjectManagerInterface $storedObjectManager,
    ) {}

    public function generateDataDump(
        DocGeneratorTemplate $template,
        int $entityId,
        array $contextGenerationDataNormalized,
        StoredObject $destinationStoredObject,
        User $creator,
        bool $clearEntityManagerDuringProcess = true,
    ): StoredObject {
        return $this->generateFromTemplate(
            $template,
            $entityId,
            $contextGenerationDataNormalized,
            $destinationStoredObject,
            $creator,
            $clearEntityManagerDuringProcess,
            true,
        );
    }

    public function generateDocFromTemplate(
        DocGeneratorTemplate $template,
        int $entityId,
        array $contextGenerationDataNormalized,
        StoredObject $destinationStoredObject,
        User $creator,
        bool $clearEntityManagerDuringProcess = true,
    ): StoredObject {
        return $this->generateFromTemplate(
            $template,
            $entityId,
            $contextGenerationDataNormalized,
            $destinationStoredObject,
            $creator,
            $clearEntityManagerDuringProcess,
            false,
        );
    }

    private function generateFromTemplate(
        DocGeneratorTemplate $template,
        int $entityId,
        array $contextGenerationDataNormalized,
        StoredObject $destinationStoredObject,
        User $creator,
        bool $clearEntityManagerDuringProcess = true,
        bool $generateDumpOnly = false,
    ): StoredObject {
        if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) {
            $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document');
            throw new ObjectReadyException();
        }

        $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [
            'entity_id' => $entityId,
            'destination_stored_object' => $destinationStoredObject->getId(),
        ]);

        $context = $this->contextManager->getContextByDocGeneratorTemplate($template);

        $entity = $this
            ->objectManagerRegistry
            ->getManagerForClass($context->getEntityClass())
            ->find($context->getEntityClass(), $entityId)
        ;

        if (null === $entity) {
            throw new RelatedEntityNotFoundException($template->getEntity(), $entityId);
        }

        $contextGenerationDataNormalized = array_merge(
            $contextGenerationDataNormalized,
            ['creator' => $creator],
            $context instanceof DocGeneratorContextWithPublicFormInterface ?
                $context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized)
                : []
        );

        $data = $context->getData($template, $entity, $contextGenerationDataNormalized);

        $destinationStoredObjectId = $destinationStoredObject->getId();

        if ($clearEntityManagerDuringProcess) {
            // we clean the entity manager
            $this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear();

            // this will force php to clean the memory
            gc_collect_cycles();
        }

        // as we potentially deleted the storedObject from memory, we have to restore it
        $destinationStoredObject = $this->objectManagerRegistry
            ->getManagerForClass(StoredObject::class)
            ->find(StoredObject::class, $destinationStoredObjectId);

        if ($generateDumpOnly) {
            $content = Yaml::dump($data, 6);
            /* @var StoredObject $destinationStoredObject */
            $destinationStoredObject
                ->setStatus(StoredObject::STATUS_READY)
            ;

            try {
                $this->storedObjectManager->write($destinationStoredObject, $content, 'application/yaml');
            } catch (StoredObjectManagerException $e) {
                $destinationStoredObject->addGenerationErrors($e->getMessage());

                throw new GeneratorException([$e->getMessage()], $e);
            }

            return $destinationStoredObject;
        }

        try {
            $templateDecrypted = $this->storedObjectManager->read($template->getFile());
        } catch (StoredObjectManagerException $e) {
            $destinationStoredObject->addGenerationErrors($e->getMessage());

            throw new GeneratorException([$e->getMessage()], $e);
        }

        try {
            $generatedResource = $this
                ->driver
                ->generateFromString(
                    $templateDecrypted,
                    $template->getFile()->getType(),
                    $data,
                    $template->getFile()->getFilename()
                );
        } catch (TemplateException $e) {
            $destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors()));
            throw new GeneratorException($e->getErrors(), $e);
        }

        /* @var StoredObject $destinationStoredObject */
        $destinationStoredObject
            ->setStatus(StoredObject::STATUS_READY)
        ;

        try {
            $this->storedObjectManager->write($destinationStoredObject, $generatedResource, $template->getFile()->getType());
        } catch (StoredObjectManagerException $e) {
            $destinationStoredObject->addGenerationErrors($e->getMessage());

            throw new GeneratorException([$e->getMessage()], $e);
        }

        $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [
            'entity_id' => $entityId,
            'destination_stored_object' => $destinationStoredObject->getId(),
        ]);

        return $destinationStoredObject;
    }
}
