<?php

namespace WordpressModels;

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use WordpressModels\DependencyInjection\CliCommandAttributes\WpCliCommandAttributesCompilerPass;
use WordpressModels\DependencyInjection\CliCommandAttributes\WpCliCommandAttributesExtension;
use WordpressModels\DependencyInjection\HookAttributes\ContainerHookRegistry;
use WordpressModels\DependencyInjection\HookAttributes\HookAttributesCompilerPass;
use WordpressModels\DependencyInjection\HookAttributes\HookAttributesExtension;
use WordpressModels\DependencyInjection\Initializer\InitializerCompilerPass;
use WordpressModels\DependencyInjection\Initializer\InitializerExtension;
use WordpressModels\DependencyInjection\Initializer\ServiceInitializer;
use WordpressModels\DependencyInjection\Metabox\MetaboxCompilerPass;
use WordpressModels\DependencyInjection\Metabox\MetaboxExtension;
use WordpressModels\DependencyInjection\Metabox\MetaboxRegistry;
use WordpressModels\DependencyInjection\RestRouteAttributes\RestRouteAttributesExtension;
use WordpressModels\DependencyInjection\RestRouteAttributes\RestRouteCompilerPass;
use WordpressModels\DependencyInjection\WordpressDoctrineExtension;

/**
 * Builder class for the service container.
 *
 * This class should be ideally loaded before `plugins_loaded` to ensure automatic compilation of the container and
 * registration of services.
 */
final class WordpressPluginContainers
{

    const WPM_CONTAINER_CLASSNAME = 'WpmContainer';

    private Container $container;
    private ConfigCacheInterface $configCache;

    public readonly string $containerFile;

    /**
     * @param bool $autoloadConfigs -- whether to automatically scan for service configurator callbacks.
     */
    public function __construct(private bool $autoloadConfigs = true)
    {
        // use separate containers for admin and public
        $this->containerFile = WPM_CONTAINER_CACHE_DIR . (is_admin() ? '/admin-container.php' : '/public-container.php');
        $this->configCache = new ConfigCache($this->containerFile, WP_DEBUG);

        if (!$this->configCache->isFresh()) {
            $this->container = new ContainerBuilder();

            // register the services
            add_action('register_services', [$this, 'registerServices']);

            // compile the container
            add_action('plugins_loaded', [$this, 'compileContainer']);
        } else {
            if (!class_exists(self::WPM_CONTAINER_CLASSNAME)) {
                // load the compiled container
                require_once $this->containerFile;
            }
            $className = self::WPM_CONTAINER_CLASSNAME;
            $this->container = new $className();

            add_action('plugins_loaded', [$this, 'initializeServices']);
        }

        add_action('init_services', [$this, 'executeServiceInitialization']);
    }

    public function initializeServices(): void
    {
        // initialize the services
        do_action('init_services', $this->container);
    }

    /**
     * Reset the container.
     *
     * Useful for testing and development.
     *
     * @return void
     */
    public function resetContainer()
    {
        if (!file_exists($this->containerFile)) {
            throw new \RuntimeException('Container file does not exist.');
        }

        do_action('before_reset_container');

        require_once $this->containerFile;
        // load the compiled container
        $class = self::WPM_CONTAINER_CLASSNAME;
        /** @var \WpmContainer container */
        $this->container = new $class();

        // garbage collect
        gc_collect_cycles();

        $this->initializeServices();

        do_action('after_reset_container');
    }

    /**
     * Get the service container.
     *
     * @return Container
     */
    public function getContainer(): Container
    {
        return $this->container;
    }

    /**
     * Run the container initialization.
     *
     * Firstly, for all active plugins, create a plugin container and register it to the global container.
     * Then, we allow plugins to register their own services to the global container.
     * Finally, we compile the global container to finalize it.
     *
     * @return void
     * @throws \Exception
     */
    public function compileContainer()
    {
        /** @var ContainerBuilder $container */
        $container = $this->container;

        do_action('register_services', $container);

        $compilerPasses = apply_filters('container_compiler_passes', [
            new InitializerCompilerPass(),
            new HookAttributesCompilerPass(),
            new WpCliCommandAttributesCompilerPass(),
            new RestRouteCompilerPass(),
            new EntityListenerPass(),
            new MetaboxCompilerPass()
        ]);
        foreach ($compilerPasses as $compilerPass) {
            $container->addCompilerPass($compilerPass);
        }

        $extensions = apply_filters('container_extensions', [
            new InitializerExtension(),
            new HookAttributesExtension(),
            new WpCliCommandAttributesExtension(),
            new RestRouteAttributesExtension(),
            new WordpressDoctrineExtension(),
            new MetaboxExtension()
        ]);
        foreach ($extensions as $extension) {
            $this->container->registerExtension($extension);
            $this->container->loadFromExtension($extension->getAlias(), []);
        }

        // compile to finalize the global container
        $container->compile();

        $dumper = new PhpDumper($container);
        if (!is_dir(WPM_CONTAINER_CACHE_DIR)) {
            mkdir(WPM_CONTAINER_CACHE_DIR);
        }
        try {
            $this->configCache->write(
                $dumper->dump([
                    'class' => self::WPM_CONTAINER_CLASSNAME,
                    'debug' => WP_DEBUG
                ]),
                $container->getResources());
        } catch (\RuntimeException $e) {
            throw new \Exception('Could not write to the cache directory', $e);
        }

        // initialize the services
        $this->initializeServices();

        // replace plugins_loaded for when resetContainer is called
        remove_action('plugins_loaded', [$this, 'compileContainer']);
        add_action('plugins_loaded', [$this, 'initializeServices']);
    }

    /**
     * Initialize services before wordpress is fully loaded.
     *
     * @return void
     * @throws \Exception
     */
    public function executeServiceInitialization()
    {
        /** @var ContainerHookRegistry $service */
        $service = $this->container->get(ContainerHookRegistry::class);
        $service->registerHooks();

        /** @var MetaboxRegistry $service */
        $service = $this->container->get(MetaboxRegistry::class);
        $service->registerMetaboxes();

        /** @var ServiceInitializer $service */
        $service = $this->container->get(ServiceInitializer::class);
        $service->initializeServices();
    }

    /**
     * Load service configurator callbacks of all MU-plugins and active valid plugins.
     *
     * @return void
     * @throws \Exception
     */
    public function registerServices()
    {
        if ($this->autoloadConfigs) {
            // glob all must use plugin config directories
            $mustUseConfigs = glob(WPMU_PLUGIN_DIR . '/*/config', GLOB_ONLYDIR);
            // glob plugin paths
            $pluginConfigs = $this->getPluginConfigs();
        } else {
            // no autoloading
            $mustUseConfigs = $pluginConfigs = [];
        }

        $additionalConfigs = apply_filters('container_config_directories', []);

        // realpath to resolve any symlinks, and array_unique to remove duplicates
        // this array is in-order, so that the wordpress-models config is loaded first
        $allDirs = array_unique(array_map('realpath', [
            __DIR__ . '/../config',
            ...$mustUseConfigs,
            ...$pluginConfigs,
            ...$additionalConfigs
        ]));

        // load services from config files
        $loader = new PhpFileLoader($this->container, new FileLocator($allDirs));
        foreach ($allDirs as $configDir) {
            if (!file_exists($configDir . '/services.php')) {
                continue;
            }
            $loader->load($configDir . '/services.php');
        }
    }

    /**
     * Delete the container cache file.
     *
     * @return void
     */
    public function invalidate(): void
    {
        unlink($this->containerFile);
        unlink($this->containerFile . '.meta');
    }

    public function __destruct()
    {
        // unregister hooks
        remove_action('register_services', [$this, 'registerServices']);
        remove_action('plugins_loaded', [$this, 'compileContainer']);
        remove_action('init_services', [$this, 'executeServiceInitialization']);
    }

    /**
     * Scan active plugin directories for config directories.
     *
     * Only active plugins are considered.
     *
     * It might be faster to use GLOB_BRACE, but it is not supported on all systems.
     *
     * @return string[]
     */
    private function getPluginConfigs(): array
    {
        $pluginConfigs = glob(WP_PLUGIN_DIR . '/*/config', GLOB_ONLYDIR);
        // filter configs for inactive plugins
        $activeValidPluginDirs = array_map(fn(string $dir) => plugin_dir_path($dir), wp_get_active_and_valid_plugins());
        return array_reduce($pluginConfigs, function (array $carry, string $dir) use ($activeValidPluginDirs) {
            if (in_array(plugin_dir_path($dir), $activeValidPluginDirs)) {
                $carry[] = $dir;
            }
            return $carry;
        }, []);
    }

}