<?php

namespace WordpressModels;

use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\Filesystem\Filesystem;
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\Pages\PageRegistry;
use WordpressModels\DependencyInjection\Pages\PagesExtension;
use WordpressModels\DependencyInjection\Pages\PagesPass;
use WordpressModels\DependencyInjection\Replacements\ResolveInstanceofConditionalsPass as CustomResolveInstanceofConditionalsPass;
use WordpressModels\DependencyInjection\RestRouteAttributes\RestRouteAttributesExtension;
use WordpressModels\DependencyInjection\RestRouteAttributes\RestRouteCompilerPass;

/**
 * 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;
    private readonly string $cacheDir;

    private readonly bool $installedAsPlugin;
    private readonly string $className;

    /**
     * @param bool $autoloadConfigs -- whether to automatically scan for service configurator callbacks.
     */
    public function __construct(private bool $autoloadConfigs = true)
    {
        $this->installedAsPlugin = defined('WPM_PLUGIN_VERSION');
        $this->className = self::WPM_CONTAINER_CLASSNAME . (is_admin() ? 'Admin' : 'Public');
        // use separate containers for admin and public
        $this->cacheDir = (defined('WPM_CONTAINER_CACHE_DIR') ? WPM_CONTAINER_CACHE_DIR : WP_CONTENT_DIR . '/cache/wpm-container');
        $this->containerFile = $this->cacheDir . '/' . $this->className . '.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($this->className)) {
                // load the compiled container
                require_once $this->containerFile;
            }
            $this->container = new $this->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;
        $this->container = new $this->className();

        // 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 MetaboxCompilerPass(),
            new PagesPass()
        ]);
        foreach ($compilerPasses as $compilerPass) {
            $container->addCompilerPass($compilerPass);
        }

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

        // replace ResolveInstanceofConditionalsPass with a custom one (on the same index)
        $passes = $container->getCompiler()->getPassConfig()->getBeforeOptimizationPasses();
        foreach ($passes as $index => $pass) {
            if ($pass instanceof ResolveInstanceofConditionalsPass) {
                $passes[$index] = new CustomResolveInstanceofConditionalsPass();
            }
        }
        $container->getCompiler()->getPassConfig()->setBeforeOptimizationPasses($passes);

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

        $dumper = new PhpDumper($container);
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0777, true);
        }
        try {
            $files = $dumper->dump([
                'class' => $this->className,
                'path' => $this->configCache->getPath(),
                'debug' => WP_DEBUG,
                'as_files' => true,
            ]);

            $rootCode = array_pop($files);
            $dir = \dirname($this->configCache->getPath()) . '/';
            $fs = new Filesystem();

            foreach ($files as $file => $code) {
                // skip preload dump in the public container, as not all files are referenced
                if (!is_admin() && str_ends_with($file, '.preload.php')) {
                    continue;
                }

                $fs->dumpFile($dir . $file, $code);
                @chmod($dir . $file, 0666 & ~umask());
            }
            $legacyFile = \dirname($dir . key($files)) . '.legacy';
            if (is_file($legacyFile)) {
                @unlink($legacyFile);
            }

            $this->configCache->write(
                $rootCode,
                $container->getResources());
        } catch (\RuntimeException $e) {
            throw new \Exception('Could not write to the cache directory', previous: $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()
    {
        // initialize hook registry
        /** @var ContainerHookRegistry $service */
        $service = $this->container->get(ContainerHookRegistry::class);
        $service->registerHooks();

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

        // initialize page registry
        /** @var PageRegistry $service */
        $service = $this->container->get(PageRegistry::class);
        $service->registerPages();

        // initialize force initialization of services
        /** @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', []);

        $paths = [
            ...$mustUseConfigs,
            ...$pluginConfigs,
            ...$additionalConfigs
        ];
        if ($this->installedAsPlugin) {
            array_unshift($paths, WPM_PLUGIN_DIR . '/config');
        }

        // 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', $paths));

        // 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');

        if (function_exists('opcache_reset')) {
            opcache_reset();
        }
    }

    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
        $activePlugins = is_multisite() ? array_merge(wp_get_active_and_valid_plugins(), wp_get_active_network_plugins()) : wp_get_active_and_valid_plugins();
        $activeValidPluginDirs = array_map(fn(string $dir) => plugin_dir_path($dir), $activePlugins);
        return array_reduce($pluginConfigs, function (array $carry, string $dir) use ($activeValidPluginDirs) {
            if (in_array(plugin_dir_path($dir), $activeValidPluginDirs)) {
                $carry[] = $dir;
            }
            return $carry;
        }, []);
    }

}
