<?php

/**
 * @file
 * Contains node_revision_delete.module.
 */

use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\node\NodeTypeInterface;

/**
 * Implements hook_help().
 */
function node_revision_delete_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {

    case 'help.page.node_revision_delete':

      $output = '';
      $output .= '<h3>' . t('Introduction') . '</h3>';
      $output .= '<p>' . t('The Node Revision Delete module lets you track and prune old revisions of content types.') . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Configuring the module') . '</dt>';
      $output .= '<dd>' . t('You can manage the module settings via the <a href=":config-page">configuration page</a>. You can configure how many revisions you want to keep per content type and configure how long revision should be kept. When saving the configuration you can optionally start a batch job to queue all content to delete revisions that are allowed to be deleted. For this you need the <em>Administer Node Revision Delete</em> permission.', [':config-page' => Url::fromRoute('node_revision_delete.admin_settings')->toString()]) . '</dd>';
      $output .= '<dt>' . t('Configuring content types') . '</dt>';
      $output .= '<dd>' . t('You can override the default settings of the module for each content type. Under the Publishing options tab of the content type, configure how many revisions you want to keep for the content type and how long revision should be kept.') . '</dd>';
      $output .= '</dl>';

      return $output;

  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function node_revision_delete_form_node_type_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeletePluginManager $plugin_manager */
  $plugin_manager = \Drupal::service('plugin.manager.node_revision_delete');
  $plugin_definitions = $plugin_manager->getDefinitions();

  // Getting the content type entity.
  /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
  $form_object = $form_state->getFormObject();
  $content_type = $form_object->getEntity();
  assert($content_type instanceof NodeTypeInterface);

  // Detect if this build is an AJAX rebuild caused by toggling the override
  // checkbox. When that happens we always show fresh defaults so the visible
  // form matches what would actually be saved.
  $triggering_element = $form_state->getTriggeringElement();
  $is_override_ajax = $form_state->isRebuilding()
    && $triggering_element !== NULL
    && ($triggering_element['#name'] ?? '') === 'node_revision_delete[override]';

  if ($is_override_ajax) {
    // Drop any user input the admin made for the plugin sub-forms so the
    // rebuilt form picks up the fresh #default_value below.
    $input = $form_state->getUserInput();
    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
      unset($input[$plugin_id]);
      $form_state->unsetValue($plugin_id);
    }
    $form_state->setUserInput($input);
  }

  // Decide which settings to display. On the initial build the form mirrors
  // what is saved on the node type. On an AJAX rebuild (or for new node
  // types) we always show the defaults.
  $has_saved_override = !$content_type->isNew() && !empty($content_type->getThirdPartySettings('node_revision_delete'));
  if ($is_override_ajax || !$has_saved_override) {
    $current_settings = [];
    foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
      $current_settings[$plugin_id] = $plugin_manager->getDefaultPluginSettings($plugin_id);
    }
  }
  else {
    $current_settings = $plugin_manager->getSettingsNodeType($content_type->id())['plugin'];
  }

  // The override state comes from the form value during a rebuild and from
  // the saved third-party settings on the initial build.
  $override = $is_override_ajax
    ? (bool) $form_state->getValue(['node_revision_delete', 'override'])
    : $has_saved_override;

  $form['node_revision_delete'] = [
    '#type' => 'details',
    '#title' => t('Node Revision Delete settings'),
    '#collapsed' => FALSE,
    '#collapsible' => TRUE,
    '#tree' => FALSE,
    '#weight' => 10,
    '#group' => 'additional_settings',
    '#attributes' => ['class' => ['node-revision-delete-settings-form']],
  ];

  // Wrap the plugin sub-forms in a container so the AJAX callback can replace
  // the whole group when override is toggled.
  $form['node_revision_delete']['plugins_wrapper'] = [
    '#type' => 'container',
    '#attributes' => ['id' => 'node-revision-delete-plugins-wrapper'],
  ];

  $form['node_revision_delete']['plugins_wrapper']['override'] = [
    '#type' => 'checkbox',
    '#title' => t('Override default settings'),
    '#default_value' => $override,
    '#attributes' => [
      'class' => ['node-revision-delete-override'],
    ],
    '#parents' => ['node_revision_delete', 'override'],
    '#ajax' => [
      'callback' => 'node_revision_delete_override_ajax_callback',
      'wrapper' => 'node-revision-delete-plugins-wrapper',
    ],
  ];

  foreach ($plugin_definitions as $plugin_id => $plugin_definition) {
    /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeleteInterface $plugin */
    $plugin = $plugin_manager->getPlugin($plugin_id, $current_settings[$plugin_id]['settings'] ?? []);
    $form['node_revision_delete']['plugins_wrapper'][$plugin_id] = [
      '#type' => 'details',
      '#title' => $plugin_definition['label'],
      '#open' => TRUE,
      // We need the #tree key for subforms to work.
      // @see https://www.drupal.org/project/drupal/issues/3053890
      '#tree' => TRUE,
      '#attributes' => [
        'class' => ['node-revision-delete-plugin-settings'],
      ],
    ];
    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status'] = [
      '#type' => 'checkbox',
      '#title' => t('Enabled'),
      '#default_value' => $current_settings[$plugin_id]['status'] ?? 0,
    ];

    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = [
      '#type' => 'container',
      '#states' => [
        'visible' => [
          ':input[name="' . $plugin_id . '[status]"]' => ['checked' => TRUE],
        ],
      ],
    ];
    $plugin_form_state = SubformState::createForSubform($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'], $form, $form_state);
    $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] = $plugin->buildConfigurationForm($form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings'] ?? [], $plugin_form_state);

    // When override is off, disable the plugin sub-form server-side. This
    // ensures Drupal substitutes the #default_value for any user input before
    // validation, so required fields (whose values come from the global
    // defaults) do not block saving the node type.
    if (!$override) {
      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['status']['#disabled'] = TRUE;
      $form['node_revision_delete']['plugins_wrapper'][$plugin_id]['settings']['#disabled'] = TRUE;
    }
  }
  $form['#attached']['library'][] = 'node_revision_delete/summaries';

  $form['#entity_builders'][] = 'node_revision_delete_form_node_type_form_builder';
}

/**
 * AJAX callback to refresh the plugin sub-forms when override is toggled.
 */
function node_revision_delete_override_ajax_callback(array $form, FormStateInterface $form_state): array {
  return $form['node_revision_delete']['plugins_wrapper'];
}

/**
 * Custom form builder to save the configuration variables.
 *
 * @param string $entity_type
 *   The entity type.
 * @param Drupal\node\NodeTypeInterface $type
 *   The entity type object.
 * @param array $form
 *   The form element.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 */
function node_revision_delete_form_node_type_form_builder(string $entity_type, NodeTypeInterface $type, array &$form, FormStateInterface $form_state) {
  $override = $form_state->getValue(['node_revision_delete', 'override']);

  /** @var \Drupal\node_revision_delete\Plugin\NodeRevisionDeletePluginManager $plugin_manager */
  $plugin_manager = \Drupal::service('plugin.manager.node_revision_delete');
  foreach ($plugin_manager->getDefinitions() as $plugin_id => $plugin_definition) {
    if ($override) {
      $plugin_form_state = SubformState::createForSubform($form['node_revision_delete']['plugins_wrapper'][$plugin_id], $form, $form_state);
      $values = $plugin_form_state->getValues();
      unset($values['override']);
      $type->setThirdPartySetting('node_revision_delete', $plugin_id, $values);
    }
    else {
      $type->unsetThirdPartySetting('node_revision_delete', $plugin_id);
    }
  }
}

/**
 * Implements hook_node_update().
 */
function node_revision_delete_node_update(NodeInterface $node) {
  $disable_automatic_queueing = \Drupal::config('node_revision_delete.settings')->get('disable_automatic_queueing');
  $has_enabled_plugins = \Drupal::service('node_revision_delete')->contentTypeHasEnabledPlugins($node->getType());
  if ($disable_automatic_queueing || !$has_enabled_plugins) {
    return;
  }

  // Add updated nodes to the queue to determine if any revisions are ready to
  // be deleted.
  $nid = (int) $node->id();
  if (\Drupal::service('node_revision_delete')->createQueueItem($nid)->queued()) {
    if (\Drupal::config('node_revision_delete.settings')->get('verbose_log')) {
      \Drupal::logger('node_revision_delete')->info('Node @id was added to the Node Revision Delete queue.', ['@id' => $node->id()]);
    }
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function node_revision_delete_form_node_revision_delete_confirm_alter(&$form, FormStateInterface &$form_state, $form_id) {
  // Get the node id from URL.
  $nid = \Drupal::routeMatch()->getRawParameter('node');
  // Get the revision id from URL.
  $revision_id = \Drupal::routeMatch()->getRawParameter('node_revision');

  // Get list of revision IDs older than current revision for the content
  // language. Use TYPE_CONTENT to ensure the correct language is used even when
  // the "Account administration pages" language negotiation is active, which
  // would cause getCurrentLanguage() (TYPE_INTERFACE) to return the admin
  // language.
  $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
  /** @var \Drupal\node_revision_delete\NodeRevisionDeleteInterface $service */
  $service = \Drupal::service('node_revision_delete');
  $vids_before = $service->getPreviousRevisionIds($nid, $revision_id, $langcode);
  // Count sum of old revisions.
  $revision_number = count($vids_before);

  // If there are old revisions.
  if ($revision_number > 0) {
    $form['revision_list'] = [
      '#type' => 'details',
      '#title' => t('Delete prior revisions'),
      '#open' => FALSE,
    ];

    $form['revision_list']['delete_prior_revisions'] = [
      '#type' => 'checkbox',
      '#title' => t('Also delete %revs_no revisions prior to this one.', ['%revs_no' => $revision_number]),
    ];

    $headers = [
      t('Revision ID'),
      [
        'data' => t('Revision'),
        // Hide the description on narrow width devices.
        'class' => [RESPONSIVE_PRIORITY_MEDIUM],
      ],
    ];
    // Cache service for later uses.
    $renderer = \Drupal::service('renderer');
    $dateFormatter = \Drupal::service('date.formatter');

    // Only load and display the 20 most recent prior revisions to avoid
    // unbounded entity loads on nodes with many revisions.
    $display_vids = array_slice($vids_before, 0, 20);
    $remaining = $revision_number - count($display_vids);

    $node_storage = \Drupal::entityTypeManager()->getStorage('node');
    $rows = [];
    /** @var \Drupal\node\NodeInterface[] $revisions */
    $revisions = $node_storage->loadMultipleRevisions($display_vids);
    krsort($revisions);
    foreach ($revisions as $revision) {
      if ($revision->hasTranslation($langcode)) {
        $revision = $revision->getTranslation($langcode);
      }
      $username = [
        '#theme' => 'username',
        '#account' => $revision->getRevisionUser(),
      ];

      // Build link to view revision.
      $date = $dateFormatter->format($revision->getRevisionCreationTime(), 'short');
      $revision_url = new Url('entity.node.revision', [
        'node' => $revision->id(),
        'node_revision' => $revision->getRevisionId(),
      ]);
      $revision_link = Link::fromTextAndUrl($date, $revision_url)->toRenderable();

      $revision_info = [
        '#type' => 'inline_template',
        '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
        '#context' => [
          'date' => $renderer->renderInIsolation($revision_link),
          'username' => $renderer->renderInIsolation($username),
          'message' => [
            '#markup' => $revision->getRevisionLogMessage(),
            '#allowed_tags' => Xss::getHtmlTagList(),
          ],
        ],
      ];

      $rows[] = [
        $revision->getRevisionId(),
        ['data' => $revision_info],
      ];
    }

    if ($remaining > 0) {
      $rows[] = [
        [
          'data' => \Drupal::translation()->formatPlural($remaining, 'And @count more revision that will also be deleted.', 'And @count more revisions that will also be deleted.'),
          'colspan' => 2,
        ],
      ];
    }

    $form['revision_list']['table_markup'] = [
      '#type' => 'table',
      '#header' => $headers,
      '#rows' => $rows,
    ];

    // Custom submit handler to handle bulk revisions delete.
    $form['#submit'][] = '_node_revision_bulk_delete_submit';
  }
}

/**
 * Custom submit handler to bulk delete revisions.
 *
 * @param array $form
 *   The form element.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state.
 */
function _node_revision_bulk_delete_submit(array &$form, FormStateInterface $form_state) {
  if ($form_state->getValue('delete_prior_revisions') == 1) {
    // Get the node id from URL.
    $nid = (int) \Drupal::routeMatch()->getRawParameter('node');
    // Get the revision id from URL.
    $vid = (int) \Drupal::routeMatch()->getRawParameter('node_revision');
    // Pass the content language to ensure the correct revisions are deleted.
    $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();

    // Call the batch.
    \Drupal::service('node_revision_delete.batch')->previousRevisionDeletionBatch($nid, $vid, $langcode);
  }
}
