<?php

/**
 * @file
 * The Social profile privacy module file.
 */

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\profile\Entity\ProfileInterface;
use Drupal\social_profile\SocialProfileNameService;
use Drupal\social_profile_privacy\Service\SocialProfilePrivacyBatchHelper;
use Drupal\social_profile_privacy\Service\SocialProfilePrivacyHelperInterface;

/**
 * Implements hook_form_alter().
 */
function social_profile_privacy_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  if (in_array($form_id, [
    'user_form',
  ])) {
    $form['#attached']['library'][] = 'social_profile_privacy/social_profile_privacy';
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_profile_privacy_form_social_profile_admin_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $config = \Drupal::config('social_profile_privacy.settings');

  // Add setting to hide Full Name for users without the `social profile privacy
  // always show full name` module.
  $form['privacy']['limit_search_and_mention'] = [
    '#type' => 'checkbox',
    '#title' => t('Limit search and mention'),
    '#description' => t("Enabling this setting causes users' full name to be hidden on the platform when the user has filled in their nickname. This setting won't hide the full name of users who didn't fill in a nickname. Users with the '%display_name' permission will still see the full name whenever available. Only users with the '%search_name' permission will find users using their full name through search or mentions.", [
      '%display_name' => t('View full name when restricted'),
      '%search_name' => t('View full name when restricted'),
    ]),
    '#default_value' => $config->get('limit_search_and_mention'),
  ];

  $form['privacy']['fields'] = [
    '#type' => 'details',
    '#title' => t('Profile fields visibility'),
    '#description' => t('Please choose which profile fields can be displayed in user profiles. Site managers can always see all the filled-in profile information.'),
    '#open' => TRUE,
    '#tree' => TRUE,
  ];

  $actions = [
    SocialProfilePrivacyHelperInterface::SHOW => t('Always show for everyone'),
    SocialProfilePrivacyHelperInterface::CONFIGURABLE => t('Show, but can be hidden by each user'),
    SocialProfilePrivacyHelperInterface::HIDE => t('Hide for others'),
  ];

  $form['privacy']['fields']['list'] = [
    '#type' => 'table',
    '#header' => array_merge([t('Field name')], $actions),
  ];

  /** @var \Drupal\social_profile_privacy\Service\SocialProfilePrivacyHelperInterface $helper */
  $helper = \Drupal::service('social_profile_privacy.helper');

  // Fields options.
  $field_options = $helper->getFieldOptions();

  foreach ($field_options as $field => $options) {
    $row = [['#plain_text' => $options['label']]];

    if ($options['access']) {
      $value = $config->get('fields.' . $field) ?: SocialProfilePrivacyHelperInterface::SHOW;
    }
    else {
      $value = SocialProfilePrivacyHelperInterface::HIDE;
    }

    foreach ($actions as $state => $label) {
      $row[] = [
        '#type' => 'radio',
        '#title' => $label,
        '#return_value' => $state,
        '#default_value' => $value === $state ? $state : NULL,
        '#disabled' => !$options['access'],
        '#parents' => ['fields', $field],
      ];
    }

    $form['privacy']['fields']['list'][] = $row;
  }

  $form['privacy']['fields']['disclaimer'] = [
    '#type' => 'text_format',
    '#title' => t('Disclaimer'),
    '#default_value' => $config->get('disclaimer.value'),
    '#format' => $config->get('disclaimer.format'),
  ];

  // Add warning for site_manager to inform about updating all the profiles
  // after form is submitted.
  // We need to get Profile name fields.
  $account_name_fields = SocialProfileNameService::getProfileNameFields();
  // Var where will we stack the Profile name fields labels.
  $profile_name_fields_labels = [];
  // Getting the Profile name fields labels.
  foreach ($account_name_fields as $account_name_field) {
    if (isset($field_options[$account_name_field]) && !empty($label = $field_options[$account_name_field]['label'])) {
      $profile_name_fields_labels[] = $label;
    }
  }
  // We need to add a warning message only if the Profile name fields settings
  // available.
  if (!empty($profile_name_fields_labels)) {
    // Make string with a list of the Profile name fields labels.
    if (count($profile_name_fields_labels) > 1) {
      $last_label = array_pop($profile_name_fields_labels);
      $profile_name_fields_labels = implode(', ', $profile_name_fields_labels);
      $profile_name_fields_labels .= " and, {$last_label}";
    }
    else {
      $profile_name_fields_labels = $profile_name_fields_labels[0];
    }

    // Add warning message.
    $form['social_profile_privacy_warning_message'] = [
      '#theme' => 'status_messages',
      '#message_list' => [
        'warning' => [
          [
            '#type' => 'html_tag',
            '#tag' => 'strong',
            '#value' => t('If visibility settings of the fields %labels will be changed, then once you submit this form, all the user profiles on the platform will be updated.', ['%labels' => $profile_name_fields_labels]),
          ],
        ],
      ],
      '#status_headings' => [
        'warning' => t('Attention!'),
      ],
      '#weight' => -1,
    ];
  }

  $form['#submit'][] = 'social_profile_privacy_admin_settings_form_submit';
}

/**
 * The submit function for social_profile_admin_settings_form().
 *
 * To save configuration of fields groups available to hiding.
 */
function social_profile_privacy_admin_settings_form_submit($form, FormStateInterface $form_state) {
  $config = \Drupal::configFactory()->getEditable('social_profile_privacy.settings');
  $config->set('limit_search_and_mention', $form_state->getValue('limit_search_and_mention'));

  $fields = $form_state->getValue('fields');

  $config->set('disclaimer', $fields['disclaimer']);

  unset($fields['disclaimer'], $fields['list']);

  // An array with fields, visibility settings that have been changed.
  $updated_fields = [];

  foreach ($fields as $field => $state) {
    if ($state !== '') {
      // Check if field settings have been changed.
      $current_value = $config->get('fields.' . $field);
      if (is_null($current_value) || ((int) $current_value !== (int) $state)) {
        $updated_fields[] = $field;
      }

      $config->set('fields.' . $field, $state);
    }
  }

  $config->save();

  // We need to get Profile name fields.
  $account_name_fields = SocialProfileNameService::getProfileNameFields();
  // If at least one field setting of the Profile name has been updated we need
  // to update all profiles.
  if (array_intersect($account_name_fields, $updated_fields)) {
    SocialProfilePrivacyBatchHelper::bulkUpdateProfileNames();
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_profile_privacy_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
  $form_object = $form_state->getFormObject();

  if ($form_object->getOperation() !== 'default') {
    return;
  }

  $config = \Drupal::config('social_profile_privacy.settings');

  $global_states = (array) $config->get('fields');

  /** @var \Drupal\user\UserInterface $account */
  $account = $form_object->getEntity();

  $form_state->set('account_id', $uid = $account->id());

  if ($value = $config->get('disclaimer.value')) {
    $form['profile_privacy']['disclaimer'] = [
      '#type' => 'markup',
      '#markup' => check_markup($value, $config->get('disclaimer.format')),
      '#weight' => -100,
    ];
  }

  $form['profile_privacy']['fields'] = [
    '#type' => 'container',
    '#weight' => 10,
  ];

  /** @var \Drupal\user\UserDataInterface $user_data */
  $user_data = \Drupal::service('user.data');

  $user_states = $user_data->get('social_profile_privacy', $uid, 'fields');

  /** @var \Drupal\social_profile_privacy\Service\SocialProfilePrivacyHelperInterface $helper */
  $helper = \Drupal::service('social_profile_privacy.helper');

  foreach ($helper->getFieldOptions($account) as $field => $options) {
    // If module social_profile_fields is enabled and the field is deselected in
    // settings then we should not show it on profile settings.
    if (
      \Drupal::moduleHandler()->moduleExists('social_profile_fields') &&
      !\Drupal::config('social_profile_fields.settings')->get("profile_profile_{$field}")
    ) {
      continue;
    }

    $state = $global_states[$field] ?? SocialProfilePrivacyHelperInterface::SHOW;
    $value = $status = TRUE;

    switch ($state) {
      case SocialProfilePrivacyHelperInterface::SHOW:
        $status = FALSE;
        break;

      case SocialProfilePrivacyHelperInterface::CONFIGURABLE:
        if (isset($user_states[$field])) {
          $value = $user_states[$field];
        }
        break;

      case SocialProfilePrivacyHelperInterface::HIDE:
        $value = $status = FALSE;
        break;
    }

    if (!$options['access']) {
      $value = $status = FALSE;
    }

    $form['profile_privacy']['fields'][$field] = [
      '#type' => 'radios',
      '#title' => $options['label'],
      '#options' => [
        1 => t('Show'),
        0 => t('Hide'),
      ],
      '#default_value' => (int) $value,
      '#disabled' => !$status,
    ];
  }

  // Ensure submission happens first to be sure that that profile name will be
  // updated correctly. In another case, the last one can be updated after the
  // second submit only.
  array_unshift($form['actions']['submit']['#submit'], '_social_profile_privacy_fields_submit');
}

/**
 * Save fields visibility options for a user.
 */
function _social_profile_privacy_fields_submit($form, FormStateInterface $form_state) {
  /** @var \Drupal\user\UserDataInterface $user_data */
  $user_data = \Drupal::service('user.data');

  $uid = $form_state->get('account_id');
  $values = (array) $user_data->get('social_profile_privacy', $uid, 'fields');

  // An array with fields, visibility settings that have been changed.
  $updated_fields = [];
  foreach ($form_state->getValue(['profile_privacy', 'fields']) as $field => $value) {
    if (!$form['profile_privacy']['fields'][$field]['#disabled']) {
      // We need to check if the setting of the current field was changed and
      // add the field to the appropriate array if so.
      if (!isset($values[$field]) || $values[$field] !== $value) {
        $updated_fields[] = $field;
      }

      $values[$field] = $value;
    }
  }

  $user_data->set('social_profile_privacy', $uid, 'fields', $values);

  $tags = ['user:' . $uid];

  $profiles = \Drupal::entityQuery('profile')
    ->condition('uid', $uid)
    ->accessCheck(TRUE)
    ->execute();

  if ($profiles) {
    $profile = reset($profiles);
    $tags[] = 'profile:' . $profile;
    $tags[] = 'entity_view:profile:' . $profile;
  }

  \Drupal::service('cache_tags.invalidator')->invalidateTags($tags);

  if (!isset($profile) || empty($updated_fields) || empty(array_intersect(SocialProfileNameService::getProfileNameFields(), $updated_fields))) {
    return;
  }
  /** @var \Drupal\profile\ProfileStorageInterface $profile_storage */
  $profile_storage = \Drupal::entityTypeManager()->getStorage('profile');
  $profile = $profile_storage->load($profile);
  if ($profile instanceof ProfileInterface) {
    SocialProfilePrivacyBatchHelper::updateProfileName($profile);
  }
}

/**
 * Returns fields the names that marked as hidden.
 *
 * @param int $uid
 *   Identifier of a user.
 *
 * @return array
 *   Array with the names of fields that marked as hidden.
 */
function social_profile_privacy_private_fields_list($uid) {
  $fields = &drupal_static(__FUNCTION__, []);

  if (isset($fields[$uid])) {
    return $fields[$uid];
  }

  /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
  $display = \Drupal::entityTypeManager()
    ->getStorage('entity_form_display')
    ->load('profile.profile.default');

  $config = \Drupal::config('social_profile_privacy.settings');
  $global_visibility = (array) $config->get('fields');

  $fields[$uid] = [];

  /** @var \Drupal\user\UserDataInterface $user_data */
  $user_data = \Drupal::service('user.data');

  $user_visibility = (array) $user_data->get('social_profile_privacy', $uid, 'fields');

  foreach ($display->getThirdPartySettings('field_group') as $field_group) {

    if (isset($field_group['children'])) {
      foreach ($field_group['children'] as $field) {
        if (!isset($global_visibility[$field])) {
          continue;
        }

        $visibility = $global_visibility[$field];

        if (
          $visibility === SocialProfilePrivacyHelperInterface::HIDE ||
          (
            $visibility === SocialProfilePrivacyHelperInterface::CONFIGURABLE &&
            isset($user_visibility[$field]) &&
            !$user_visibility[$field]
          )
        ) {
          $fields[$uid][] = $field;
        }
      }
    }
  }

  return $fields[$uid];
}

/**
 * Implements hook_entity_field_access().
 */
function social_profile_privacy_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  if ($operation == 'view' && $field_definition->getTargetEntityTypeId() == 'profile' && $items !== NULL) {
    $uid = $items->getEntity()->get('uid')->target_id;
    $fields = social_profile_privacy_private_fields_list($uid);

    // If owner.
    $access = ($uid == $account->id());
    // If field is not hidden.
    $access = $access || !in_array($field_definition->getName(), $fields);
    // If user has access to view hidden fields.
    $access = $access || $account->hasPermission('social profile privacy view hidden fields');

    $access_result = AccessResult::forbiddenIf(!$access);
    return $access_result->cachePerUser();
  }

  return AccessResult::neutral();
}

/**
 * Implements hook_social_user_name_display_suggestions_alter().
 *
 * Given that we're being extra strict about real names.
 * When there is both a full name and a nickname then we combine the two for
 * users that are allowed to see a full name even when there's a nickname.
 */
function social_profile_privacy_social_user_name_display_suggestions_alter(array &$suggestions, AccountInterface $account) {
  $config = \Drupal::config('social_profile_privacy.settings');

  if (
    $config->get('limit_search_and_mention')
    && isset($suggestions['full_name'], $suggestions['nickname'])
    && \Drupal::currentUser()->hasPermission('social profile privacy always show full name')
  ) {
    $suggestions['nickname_with_full_name'] = [
      'weight' => -PHP_INT_MAX,
      'name' => $suggestions['nickname']['name'] . ' (' . $suggestions['full_name']['name'] . ')',
    ];
  }
}

/**
 * Re-saves search indices.
 *
 * This triggers the save for search indices that have profile entities as data.
 * This ensures that the RestrictedNameProcessor is properly added.
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Core\Entity\EntityStorageException
 */
function _social_profile_privacy_resave_search_indexes() {
  // If the search api module is not installed we have nothing to do.
  if (!\Drupal::moduleHandler()->moduleExists('search_api')) {
    return;
  }

  // We load all indexes, we assume there will never be hundreds of search
  // indexes which would create its own problems for a site.
  $indexes = \Drupal::entityTypeManager()
    ->getStorage('search_api_index')
    ->loadMultiple();

  /** @var \Drupal\search_api\IndexInterface $index */
  foreach ($indexes as $index) {
    // Check if the search index has profile entities as data source.
    if ($index->isValidDatasource('entity:profile')) {
      // Disable and enable the index to ensure that the RestrictedNameProcessor
      // has the chance to add the field.
      $index->save();
    }
  }
}
