<?php

declare(strict_types=1);

namespace Drush\Commands\config;

use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\CommandError;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Drupal\config\StorageReplaceDataWrapper;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\ImportStorageTransformer;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drush\Attributes as CLI;
use Drush\Boot\DrupalBootLevels;
use Drush\Commands\AutowireTrait;
use Drush\Commands\core\DocsCommands;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\UserAbortException;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Filesystem\Path;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class ConfigImportCommands extends DrushCommands
{
    use AutowireTrait;

    const IMPORT = 'config:import';

    public function getConfigManager(): ConfigManagerInterface
    {
        return $this->configManager;
    }

    public function getConfigStorage(): StorageInterface
    {
        return $this->configStorage;
    }

    public function getConfigStorageSync(): StorageInterface
    {
        return $this->configStorageSync;
    }

    public function setConfigStorageSync(?StorageInterface $syncStorage): void
    {
        $this->configStorageSync = $syncStorage;
    }

    public function getConfigCache(): CacheBackendInterface
    {
        return $this->configCache;
    }

    public function getModuleHandler(): ModuleHandlerInterface
    {
        return $this->moduleHandler;
    }

    public function getEventDispatcher(): EventDispatcherInterface
    {
        return $this->eventDispatcher;
    }

    public function getLock(): LockBackendInterface
    {
        return $this->lock;
    }

    public function getConfigTyped(): TypedConfigManagerInterface
    {
        return $this->configTyped;
    }

    public function getModuleInstaller(): ModuleInstallerInterface
    {
        return $this->moduleInstaller;
    }

    public function getThemeHandler(): ThemeHandlerInterface
    {
        return $this->themeHandler;
    }

    public function getStringTranslation(): TranslationInterface
    {
        return $this->stringTranslation;
    }

    public function setImportTransformer(ImportStorageTransformer $importStorageTransformer): void
    {
        $this->importStorageTransformer = $importStorageTransformer;
    }

    public function hasImportTransformer(): bool
    {
        return isset($this->importStorageTransformer);
    }

    public function getImportTransformer(): ?ImportStorageTransformer
    {
        return $this->importStorageTransformer;
    }

    public function getModuleExtensionList(): ModuleExtensionList
    {
        return $this->moduleExtensionList;
    }

    public function getThemeExtensionList(): ThemeExtensionList
    {
        return $this->themeExtensionList;
    }

    public function __construct(
        protected ConfigManagerInterface $configManager,
        #[Autowire('config.storage')]
        protected StorageInterface $configStorage,
        #[Autowire('cache.config')]
        protected CacheBackendInterface $configCache,
        protected ModuleHandlerInterface $moduleHandler,
        protected EventDispatcherInterface $eventDispatcher,
        #[Autowire('lock')]
        protected LockBackendInterface $lock,
        protected TypedConfigManagerInterface $configTyped,
        protected ModuleInstallerInterface $moduleInstaller,
        protected ThemeHandlerInterface $themeHandler,
        protected TranslationInterface $stringTranslation,
        protected ModuleExtensionList $moduleExtensionList,
        protected ThemeExtensionList $themeExtensionList,
        #[Autowire('config.storage.sync')]
        protected StorageInterface $configStorageSync,
        protected ?ImportStorageTransformer $importStorageTransformer = null
    ) {
        parent::__construct();
    }

    /**
     * Import config from the config directory.
     */
    #[CLI\Command(name: self::IMPORT, aliases: ['cim', 'config-import'])]
    #[CLI\Option(name: 'diff', description: 'Show preview as a diff.')]
    #[CLI\Option(name: 'source', description: 'An arbitrary directory that holds the configuration files.')]
    #[CLI\Option(name: 'partial', description: 'Allows for partial config imports from the source directory. Only updates and new configs will be processed with this flag (missing configs will not be deleted). No config transformation happens.')]
    #[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
    #[CLI\Usage(name: 'drush config:import', description: 'Update Drupal\'s configuration so it matches the contents of the config directory.')]
    #[CLI\Usage(name: 'drush config:import --partial --source=/app/config', description: 'Import from the /app/config directory which typically contains one or a few yaml files.')]
    #[CLI\Usage(name: 'cat tmp.yml | drush config:set --input-format=yaml user.mail ? -', description: 'Update the <info>user.mail</info> config object in its entirety.')]
    #[CLI\Topics(topics: [DocsCommands::DEPLOY])]
    public function import(array $options = ['source' => self::REQ, 'partial' => false, 'diff' => false])
    {
        // Determine source directory.
        $source_storage_dir = ConfigCommands::getDirectory($options['source']);

        // Prepare the configuration storage for the import.
        if ($source_storage_dir === Path::canonicalize(Settings::get('config_sync_directory'))) {
            $source_storage = $this->getConfigStorageSync();
        } else {
            $source_storage = new FileStorage($source_storage_dir);
        }

        // Determine $source_storage in partial case.
        $active_storage = $this->getConfigStorage();
        if ($options['partial']) {
            $replacement_storage = new StorageReplaceDataWrapper($active_storage);
            foreach ($source_storage->listAll() as $name) {
                $data = $source_storage->read($name);
                $replacement_storage->replaceData($name, $data);
            }
            $source_storage = $replacement_storage;
        } elseif ($this->hasImportTransformer()) {
            // Use the import transformer if it is available. (Drupal ^8.8)
            // Drupal core does not apply transformations for single imports.
            // And in addition the StorageReplaceDataWrapper is not compatible
            // with StorageCopyTrait::replaceStorageContents.
            $source_storage = $this->getImportTransformer()->transform($source_storage);
        }

        $storage_comparer = new StorageComparer($source_storage, $active_storage);
        if (!$storage_comparer->createChangelist()->hasChanges()) {
            $this->logger()->notice(('There are no changes to import.'));
            return;
        }

        if (!$options['diff']) {
            $change_list = [];
            foreach ($storage_comparer->getAllCollectionNames() as $collection) {
                $change_list[$collection] = $storage_comparer->getChangelist(null, $collection);
            }
            $table = ConfigCommands::configChangesTable($change_list, $this->output());
            $table->render();
        } else {
            $output = ConfigCommands::getDiff($active_storage, $source_storage, $this->output());

            $this->output()->writeln($output);
        }

        if (!$this->io()->confirm(dt('Import the listed configuration changes?'))) {
            throw new UserAbortException();
        }
        return drush_op([$this, 'doImport'], $storage_comparer);
    }

    // Copied from submitForm() at /core/modules/config/src/Form/ConfigSync.php
    public function doImport($storage_comparer): void
    {
        $config_importer = new ConfigImporter(
            $storage_comparer,
            $this->getEventDispatcher(),
            $this->getConfigManager(),
            $this->getLock(),
            $this->getConfigTyped(),
            $this->getModuleHandler(),
            $this->getModuleInstaller(),
            $this->getThemeHandler(),
            $this->getStringTranslation(),
            $this->getModuleExtensionList(),
            $this->getThemeExtensionList()
        );
        if ($config_importer->alreadyImporting()) {
            $this->logger()->warning('Another request may be synchronizing configuration already.');
        } else {
            try {
                // This is the contents of \Drupal\Core\Config\ConfigImporter::import.
                // Copied here so we can log progress.
                if ($config_importer->hasUnprocessedConfigurationChanges()) {
                    $sync_steps = $config_importer->initialize();
                    foreach ($sync_steps as $step) {
                        $context = [];
                        do {
                            $config_importer->doSyncStep($step, $context);
                            if (isset($context['message'])) {
                                $this->logger()->notice(
                                    str_replace('Synchronizing', 'Synchronized', (string)$context['message'])
                                );
                            }
                        } while ($context['finished'] < 1);
                    }
                    // Clear the cache of the active config storage.
                    $this->getConfigCache()->deleteAll();
                }

                if ($config_importer->getErrors()) {
                    throw new ConfigException('Errors occurred during import');
                }

                $this->logger()->success('The configuration was imported successfully.');
            } catch (ConfigException $e) {
                // Return a negative result for UI purposes. We do not differentiate
                // between an actual synchronization error and a failed lock, because
                // concurrent synchronizations are an edge-case happening only when
                // multiple developers or site builders attempt to do it without
                // coordinating.
                $message = 'The import failed due to the following reasons:' . "\n";
                $message .= implode("\n", $config_importer->getErrors());

                throw new \Exception($message, $e->getCode(), $e);
            } finally {
                // Importing config might trigger batch operations (such as when installing and uninstalling modules).
                // @see \Drush\Commands\pm\PmCommands::install()
                if (batch_get()) {
                    $this->logger()->notice('Running batch operations...');
                    drush_backend_batch_process();
                }
            }
        }
    }

    /**
     * Validate partial and source options.
     */
    #[CLI\Hook(type: HookManager::ARGUMENT_VALIDATOR, target: self::IMPORT)]
    public function validate(CommandData $commandData)
    {
        $msgs = [];
        if ($commandData->input()->getOption('partial') && !\Drupal::moduleHandler()->moduleExists('config')) {
            $msgs[] = 'Enable the config module in order to use the --partial option.';
        }

        if ($source = $commandData->input()->getOption('source')) {
            if (!file_exists($source)) {
                $msgs[] = 'The source directory does not exist.';
            }
            if (!is_dir($source)) {
                $msgs[] = 'The source is not a directory.';
            }
        }

        if ($msgs) {
            return new CommandError(implode(' ', $msgs));
        }
    }
}
