<?php

declare(strict_types=1);

namespace Gls\GlsPoland\PrestaShop\Installer;

use Gls\GlsPoland\Configuration\Initializer\DoctrineMappingsInitializer;
use Gls\GlsPoland\Configuration\Initializer\DoctrineTypesInitializer;
use Gls\GlsPoland\Configuration\Initializer\EnvInitializer;
use Gls\GlsPoland\DependencyInjection\CompilerPass\ProvideFactoriesCompilerPass;
use Gls\GlsPoland\PrestaShop\MessageHandler\Carrier\Assembler\CarrierAssemblerInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CheckDefinitionValidityPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;

final class InstallerFactory
{
    private $module;
    private $appContainer;
    private $installerContainer;

    public function __construct(\Module $module, ContainerInterface $container)
    {
        $this->module = $module;
        $this->appContainer = $container;
    }

    /**
     * @return InstallerInterface&UninstallerInterface
     */
    public function createInstaller(): InstallerInterface
    {
        return $this->getContainer()->get(InstallerInterface::class);
    }

    private function getContainer(): ContainerInterface
    {
        return $this->installerContainer ?? ($this->installerContainer = $this->buildContainer());
    }

    private function buildContainer(): ContainerInterface
    {
        $this->initConfiguration();

        $parameterBag = $this->createInstallerParameterBag();
        $container = new ContainerBuilder($parameterBag);

        $fileLocator = new FileLocator($this->module->getLocalPath() . 'config/');
        $loader = new YamlFileLoader($container, $fileLocator);
        $loader->load('installer.yml');

        $passConfig = $container->getCompilerPassConfig();
        $optimizationPasses = $this->setUpOptimizationPasses($passConfig->getOptimizationPasses());
        $passConfig->setOptimizationPasses($optimizationPasses);

        $container
            ->registerForAutoconfiguration(InstallerInterface::class)
            ->addTag('gls_poland.installer');
        $container
            ->registerForAutoconfiguration(UninstallerInterface::class)
            ->addTag('gls_poland.uninstaller');
        $container
            ->registerForAutoconfiguration(DatabaseMigrationInterface::class)
            ->addTag('gls_poland.db_migration');
        $container
            ->registerForAutoconfiguration(CarrierAssemblerInterface::class)
            ->addTag('gls_poland.carrier_assembler');

        $container->compile(true);
        $container->set('app_container', $this->appContainer);

        return $container;
    }

    private function createInstallerParameterBag(): ParameterBag
    {
        $parameterBag = new ParameterBag();

        // Keep installer container isolated from unrelated app env placeholders
        // (e.g. PS_EDITION_BASIC_ENV), while still providing required module params.
        foreach ([
            'kernel.cache_dir',
            'kernel.environment',
            'database_prefix',
            'database_engine',
        ] as $name) {
            if ($this->appContainer->hasParameter($name)) {
                $parameterBag->set($name, $this->appContainer->getParameter($name));
            }
        }

        if (!$parameterBag->has('database_prefix')) {
            $parameterBag->set('database_prefix', defined('_DB_PREFIX_') ? _DB_PREFIX_ : 'ps_');
        }

        if (!$parameterBag->has('database_engine')) {
            $parameterBag->set('database_engine', defined('_MYSQL_ENGINE_') ? _MYSQL_ENGINE_ : 'InnoDB');
        }

        return $parameterBag;
    }

    private function setUpOptimizationPasses(array $passes): array
    {
        $additionalPass = new ProvideFactoriesCompilerPass($this->appContainer, 'app_container');
        foreach ($passes as $i => $pass) {
            if (!$pass instanceof CheckDefinitionValidityPass) {
                continue;
            }

            array_splice($passes, $i, 1, [$pass, $additionalPass]);

            return $passes;
        }

        $passes[] = $additionalPass;

        return $passes;
    }

    private function initConfiguration(): void
    {
        foreach ($this->getConfigInitializers() as $initializer) {
            $initializer->init();
        }
    }

    private function getConfigInitializers(): \Generator
    {
        $doctrine = $this->appContainer->get('doctrine');
        $entityManager = $doctrine->getManager();

        yield new EnvInitializer($this->module, null, $this->appContainer->getParameter('kernel.environment'));
        yield new DoctrineMappingsInitializer($entityManager, $this->module, $doctrine);
        yield new DoctrineTypesInitializer();
    }
}
