<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

declare(strict_types=1);

namespace InPost\International\Installer;

use Doctrine\Bundle\DoctrineBundle\Mapping\MappingDriver;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\KernelEvents;

if (!defined('_PS_VERSION_')) {
    exit;
}

/**
 * @internal
 */
final class RuntimeConfigAdjustingInstaller implements InstallerInterface, UninstallerInterface
{
    /**
     * @var InstallerInterface|UninstallerInterface
     */
    private $installer;

    /**
     * @var string|null
     */
    private $entityNamespace;

    /**
     * @var string
     */
    private $psVersion;

    /**
     * @var bool
     */
    private $configModified = false;

    public function __construct(InstallerInterface $installer, string $entityNamespace = null, string $psVersion = _PS_VERSION_)
    {
        $this->installer = $installer;
        $this->entityNamespace = $entityNamespace;
        $this->psVersion = $psVersion;
    }

    public function install(\Module $module): void
    {
        $this->modifyConfig($module);
        $this->installer->install($module);
    }

    public function uninstall(\Module $module, bool $keepData): void
    {
        if (!$this->installer instanceof UninstallerInterface) {
            return;
        }

        $this->modifyConfig($module);
        $this->installer->uninstall($module, $keepData);
    }

    private function modifyConfig(\Module $module): void
    {
        if ($this->configModified) {
            return;
        }

        $container = $module->getContainer();

        $this->setUpRoutingLoaderResolver($container);
        $this->resetStaticCircularReferencesBeforeCacheClear($container);
        $this->initORMMappingDriver($container, $module);

        $this->configModified = true;
    }

    /**
     * Accesses the public "routing.loader" service to provide a @see \Symfony\Component\Config\Loader\LoaderResolverInterface
     * to the routing configuration loader used by @see \PrestaShop\PrestaShop\Adapter\Module\Tab\ModuleTabRegister
     */
    private function setUpRoutingLoaderResolver(ContainerInterface $container): void
    {
        try {
            $container->get('routing.loader');
        } catch (\Exception $e) {
            // ignore silently
        }
    }

    /**
     * Dumping container metadata on PHP versions before 8.1 might cause the script to exceed memory limit
     * in @see \Symfony\Component\Config\Resource\ReflectionClassResource::generateSignature()
     * due to https://bugs.php.net/bug.php?id=80821 if @see \Module::$_INSTANCE is not empty.
     */
    private function resetStaticCircularReferencesBeforeCacheClear(ContainerInterface $container): void
    {
        if (80100 <= PHP_VERSION_ID || \Tools::version_compare($this->psVersion, '1.7.8')) {
            return;
        }

        try {
            /** @var EventDispatcherInterface $dispatcher */
            $dispatcher = $container->get('event_dispatcher');

            $dispatcher->addListener(
                'cli' === PHP_SAPI ? ConsoleEvents::TERMINATE : KernelEvents::TERMINATE,
                self::getStaticCircularReferencesClearer(),
                -1000
            );
        } catch (\Exception $e) {
            // ignore silently
        }
    }

    private function initORMMappingDriver(ContainerInterface $container, \Module $module): void
    {
        if (null === $this->entityNamespace) {
            return;
        }

        /** @var EntityManagerInterface $entityManager */
        $entityManager = $container->get('doctrine')->getManager();
        /** @var MappingDriverChain|MappingDriver|null $mappingDriver */
        $mappingDriver = $entityManager->getConfiguration()->getMetadataDriverImpl();

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

        if (is_callable([$mappingDriver, 'getDriver'])) {
            $mappingDriver = $mappingDriver->getDriver();
        }

        assert($mappingDriver instanceof MappingDriverChain);

        if (array_key_exists($this->entityNamespace, $mappingDriver->getDrivers())) {
            return;
        }

        $driver = new AnnotationDriver(new AnnotationReader(), [$module->getLocalPath() . 'src/Entity/']);
        $mappingDriver->addDriver($driver, $this->entityNamespace);
    }

    private static function getStaticCircularReferencesClearer(): callable
    {
        if (is_callable([\Module::class, 'resetStaticCache'])) {
            return [\Module::class, 'resetStaticCache'];
        }

        return \Closure::bind(static function () {
            \Module::$_INSTANCE = [];
        }, null, \Module::class);
    }
}
