<?php

/**
 * @file
 * The social user module alterations.
 */

use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\profile\Entity\ProfileInterface;
use Drupal\social_group\SocialGroupHelperService;
use Drupal\social_user\Entity\User;
use Drupal\social_user\Plugin\Action\SocialAddRoleUser;
use Drupal\social_user\Plugin\Action\SocialBlockUser;
use Drupal\social_user\Plugin\Action\SocialRemoveRoleUser;
use Drupal\user\Plugin\Action\BlockUser;
use Drupal\user\UserInterface;
use Drupal\views\ViewExecutable;
use Drupal\social_user\SocialUserSearchContentBlockAlter;

/**
 * Implements hook_entity_type_alter().
 */
function social_user_entity_type_alter(array &$entity_types) {
  if (isset($entity_types['user'])) {
    $entity_types['user']->setClass(User::class);
  }
}

/**
 * Implements hook_action_info_alter().
 *
 * @see hook_action_info_alter()
 */
function social_user_action_info_alter(&$definitions) {
  // Swaps stadard block user action with our implementation.
  // If another module has already swapped out those classes,
  // though, we'll be polite and do nothing.
  if (isset($definitions['user_block_user_action']['class']) && $definitions['user_block_user_action']['class'] == BlockUser::class) {
    $definitions['user_block_user_action']['class'] = SocialBlockUser::class;
  }
  // The role_delegation module makes sure that the SM can't grant
  // himself/herself that administrator role. However, the VBO bulk action
  // doesn't take this well into account (yet). Therefore we override the
  // AddRoleUser.php class from core, to remove the option that should not
  // be available.
  if (isset($definitions['user_add_role_action']['class'])) {
    $definitions['user_add_role_action']['class'] = SocialAddRoleUser::class;
  }
  if (isset($definitions['user_remove_role_action']['class'])) {
    $definitions['user_remove_role_action']['class'] = SocialRemoveRoleUser::class;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * @see \Drupal\user\UserLoginForm
 */
function social_user_form_user_login_form_alter(&$form, FormStateInterface $form_state) {
  // Ensure fields have weights so other forms can add items.
  if (isset($form['account'])) {
    if (isset($form['account']['name_or_mail'])) {
      $form['account']['name_or_mail']['#weight'] = -100;
    }
    if (isset($form['account']['pass'])) {
      $form['account']['pass']['#weight'] = -50;
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_user_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state): void {
  if ($form['#id'] === 'views-exposed-form-user-admin-people-page-1') {
    $main_container = &$form['filters']['children']['container-root']['children']['container-container-0']['children'];
    $secondary_container = &$form['filters']['children']['container-root']['children']['container-container-1']['children'];

    if ($main_container) {
      $main_container['group']['#select2']['closeOnSelect'] = FALSE;

      // Add wrapper to the filter and reset button.
      $main_container['submit']['#prefix'] = '<div class="form-actions">';
      $main_container['reset']['#suffix'] = '</div>';
    }

    if ($secondary_container) {
      $permission_filter_container = &$secondary_container['container-container-9'];

      if ($permission_filter_container['children']['permission']) {
        // Hide Permission filter container if the filter is hidden.
        if (isset($permission_filter_container['children']['permission']['#printed'])) {
          hide($permission_filter_container);
        }
        else {
          // Change "- Any -" value to "Select" in Permission field.
          // @todo Follow issue https://www.drupal.org/project/better_exposed_filters/issues/3412550.
          $permission_filter_container['children']['permission']['#options']['All'] = t('Select...');
        }
      }
    }
  }
}

/**
 * Returns array with titles of all groups, ordered by their label.
 */
function _social_user_get_groups(): array {
  return call_user_func([SocialGroupHelperService::class, 'getGroups']);
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * @see \Drupal\user\RegisterForm
 */
function social_user_form_user_register_form_alter(&$form, FormStateInterface $form_state) {
  // By default notify the user of the new account.
  if (isset($form['account']['notify']) && $form['account']['notify']['#access'] === TRUE) {
    $form['account']['notify']['#default_value'] = 1;
  }

  // Treat the account container as a separate fieldset so it is properly themed
  // by the socialbase theme.
  if (isset($form['account'])) {
    $form['account']['#type'] = 'fieldset';
    $form['account']['#title'] = new TranslatableMarkup('Sign up with <b>email</b>');

    // If we have a help text then we display it to the user.
    $signup_help = \Drupal::config('social_user.settings')->get('signup_help');
    if (!empty($signup_help)) {
      $form['account']['#description'] = $signup_help;
    }

    // Ensure fields have weights so other forms can add items.
    if (isset($form['account']['mail'])) {
      $form['account']['mail']['#weight'] = -150;
    }
    if (isset($form['account']['name'])) {
      $form['account']['name']['#weight'] = -100;
    }
    if (isset($form['account']['pass'])) {
      $form['account']['pass']['#weight'] = -50;
    }

    $link_options = [];

    // Preserve the destination parameter when a user logs in instead.
    $request = \Drupal::request();
    if ($request->query->has('destination')) {
      $link_options['query'] = [
        'destination' => $request->query->get('destination'),
      ];
    }

    // Ensure this is not shown for site managers when creating accounts through
    // the admin interface.
    if (\Drupal::currentUser()->isAnonymous()) {
      $login_link = Link::createFromRoute(new TranslatableMarkup('Log in'), 'user.login', [], $link_options)->toString();

      $form['account']['login-link'] = [
        '#markup' => new TranslatableMarkup("Have an account already? @link", ["@link" => $login_link]),
        '#weight' => 1000,
        '#cache' => [
          'contexts' => ['url.query_args'],
        ],
      ];
    }
  }

  // Add an extra validation option, to check for existing data.
  $form['#validate'][] = 'social_user_register_validate';
}

/**
 * Validate function for the user register form.
 */
function social_user_register_validate(&$form, FormStateInterface $form_state) {
  // Fetch input.
  $input = $form_state->getValues();
  // Check if user data with the input can be found.
  $mail_exists = user_load_by_mail($input['mail']);
  $name_exists = user_load_by_name($input['name']);

  // Check if mail or username already exist.
  if ($mail_exists || $name_exists) {
    // If either the username or mail already exists in the DB, we clear ALL
    // existing messages, making sure nothing about this is being disclosed.
    $form_state->clearErrors();

    // Check if the email address already exist. Set a generic message
    // regarding the problem.
    if ($mail_exists) {
      $form_state->setErrorByName('mail', t("Due to privacy concerns, we can't disclose the existence of registered email addresses. Please make sure the email address is entered correctly and try again."));
    }

    // Check if the username already exist. Set a generic message regarding
    // the problem.
    if ($name_exists) {
      $form_state->setErrorByName('name', t('The entered username already exists or has an incorrect format. Please try again.'));
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * @see \Drupal\user\AccountForm
 */
function social_user_form_user_form_alter(&$form, FormStateInterface $form_state) {
  // Add forcing of changing password, message to user
  // if they logged in via a one-time login link.
  // See AccountForm.php this is where user_pass_reset gets set. Only when user
  // uses a one time login link it's true.
  if ($form_state->get('user_pass_reset')) {
    // We prepend it because a user first has to be saved :) and the profile
    // user save needs to fire first!
    $form['actions']['submit']['#submit'][] = '_social_user_pass_reset_submit';

    // We have to pass some values, because at this point the user is still
    // unsaved and the createdTime and the changedTime are still the same!
    // So it's actually the first time the user logs in and we can do our
    // redirect.
    $user = \Drupal::routeMatch()->getParameter('user');

    $storage = \Drupal::entityTypeManager()->getStorage('profile');

    /** @var \Drupal\profile\Entity\ProfileInterface $profile */
    $profile = $storage->loadByUser($user, 'profile');

    if (!($profile instanceof ProfileInterface)) {
      $profile = &$user;
    }

    if ($profile->getCreatedTime() == $profile->getChangedTime()) {

      // Remove unwanted message.
      $message = 'You have just used your one-time login link. It is no longer necessary to use this link to log in. Please change your password.';
      if (isset($_SESSION['messages'])) {
        foreach ($_SESSION['messages'] as $type => $messages) {
          if ($type == 'status') {
            $key = array_search($message, $messages);
            if ($key !== FALSE) {
              unset($_SESSION['messages'][$type][$key]);
            }
          }
        }

        if (empty($_SESSION['messages']['status'])) {
          unset($_SESSION['messages']['status']);
        }
      }

      $form_state->set('first_time_login', TRUE);
    }
  }

  $form['timezone']['#title'] = '';

  /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
  $form_object = $form_state->getFormObject();
  if ($form_object->getEntity()->isNew()) {
    // Special handling for the inevitable "Verified user" role in case when
    // "Verified immediately" setting is enabled.
    $config = \Drupal::config('social_user.settings');
    if ($config->get('verified_immediately')) {
      $form['account']['roles']['verified'] = [
        '#default_value' => TRUE,
        '#disabled' => TRUE,
      ];
    }
  }
}

/**
 * Submit function for resetting password form.
 */
function _social_user_pass_reset_submit(array $form, FormStateInterface $form_state): void {
  $storage = $form_state->getValues();
  $submitted_user_id = $storage['uid'] ?? '';

  // Only when there is actual user, and the actual user is changing its own
  // account we redirect. When you're editing others you don't want this.
  if (!empty($submitted_user_id) && \Drupal::currentUser()->id() == $submitted_user_id) {
    $first_time_login = $form_state->get('first_time_login');

    // If created & changed user time are the same, the user has never submitted
    // the user save form before. Which means we can redirect to user profile.
    if ($first_time_login) {
      $form_state->setRedirect(
        'profile.user_page.single',
        [
          'user' => $submitted_user_id,
          'profile_type' => 'profile',
          [],
        ]);
      $form_state->set('first_time_login', FALSE);
    }
    else {
      // We redirect them to the home page stream.
      $form_state->setRedirect('social_core.homepage');
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_user_form_views_form_user_admin_people_page_1_alter(&$form, FormStateInterface $form_state) {
  if (!empty($form['header']['user_bulk_form']['action']['#options'])) {
    $actions = $form['header']['user_bulk_form']['action']['#options'];
    $block_action = ['user_block_user_action' => $actions['user_block_user_action']];
    $current_user = \Drupal::currentUser();
    if (!$current_user->hasPermission('administer users')) {
      $actions = [];
    }
    if (empty($actions) && $current_user->hasPermission('block users')) {
      $actions = $block_action;
    }
    $form['header']['user_bulk_form']['action']['#options'] = $actions;
  }
}

/**
 * Check if an users with the input field for name or mail field is blocked.
 *
 * @param string $name_or_mail
 *   Username or email address.
 *
 * @return bool
 *   TRUE if blocked FALSE if not blocked
 */
function social_user_is_blocked($name_or_mail) {
  $is_blocked_name = (bool) \Drupal::entityQuery('user')
    ->condition('name', $name_or_mail)
    ->condition('status', 0)
    ->accessCheck(FALSE)
    ->execute();

  $is_blocked_mail = (bool) \Drupal::entityQuery('user')
    ->condition('mail', $name_or_mail)
    ->condition('status', 0)
    ->accessCheck(FALSE)
    ->execute();

  if ($is_blocked_name || $is_blocked_mail) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_entity_base_field_info_alter().
 */
function social_user_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
  // Add the custom Social username constraint.
  if ($entity_type->id() == 'user' && isset($fields['name'])) {
    $fields['name']->addConstraint('SocialUserName');
  }
}

/**
 * Implements hook_user_cancel_methods_alter().
 */
function social_user_user_cancel_methods_alter(&$methods) {
  $methods['user_cancel_reassign']['title'] = t('Delete your account and anonymize all other content.');
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_user_form_user_cancel_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Fetch the current user.
  $account = \Drupal::currentUser();
  // Check if the user has permissions.
  if ($account->hasPermission('administer account settings') === FALSE) {
    // Remove the option to cancel account and delete all related content.
    unset($form['user_cancel_method']['#options']['user_cancel_delete']);
  }
}

/**
 * Implements hook_theme().
 */
function social_user_theme() {
  return [
    'unwrapped_container' => [
      'render element' => 'element',
    ],
  ];
}

/**
 * Prepares variables for unwrapped container templates.
 *
 * Default template: unwrapped-container.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #id, #attributes, #children.
 *
 * @see \template_preprocess_container()
 */
function template_preprocess_unwrapped_container(array &$variables) {
  // The UnwrappedContaienr element exists to provide a different template from
  // the container which is used by the AccountHeaderElement which is why we the
  // original container implementation works fine for us. The div is then simply
  // not added in the template.
  template_preprocess_container($variables);
}

/**
 * Implements hook_block_view_BASE_BLOCK_ID_alter().
 */
function social_user_block_view_search_content_block_alter(array &$build, BlockPluginInterface $block) {
  // Add pre render to search content block in the header.
  $build['#pre_render'][] = [
    SocialUserSearchContentBlockAlter::class,
    'preRender',
  ];
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function social_user_menu_local_tasks_alter(&$data, $route_name) {
  // Change the default 'View' tab title.
  if (isset($data['tabs'][0]['entity.user.canonical'])) {
    unset($data['tabs'][0]['entity.user.canonical']);
  }

  // Remove Edit tab. Edit will always go through Floating Edit Button.
  if (isset($data['tabs'][0]['entity.user.edit_form'])) {
    unset($data['tabs'][0]['entity.user.edit_form']);
  }

  // Keep consistent sorting of 2nd navbar items.
  if (isset($data['tabs'][0]['social_user.events'])) {
    $data['tabs'][0]['social_user.events']['#weight'] = -2;
  }
  if (isset($data['tabs'][0]['social_user.topics'])) {
    $data['tabs'][0]['social_user.topics']['#weight'] = -1;
  }

}

/**
 * Implements hook_tokens_alter().
 *
 * This is a fallback for when the user object is empty and the display name and
 * URL tokens are not filled in by the other token replacements.
 * In cases like this the account is cancelled, but the message remains behind.
 */
function social_user_tokens_alter(&$replacements, $context, $bubbleable_metadata) {
  // Change the display name to that of the Anonymous user when the display name
  // token was not replaced.
  if ((isset($context['tokens']['display-name']) && empty($replacements[$context['tokens']['display-name']])) &&
    (array_key_exists('user', $context['data']) && $context['data']['user'] === NULL)) {
    $replacements[$context['tokens']['display-name']] = \Drupal::configFactory()->get('user.settings')->get('anonymous');
  }
  // Empty the URL so it doesn't break rendering when the URL token was not
  // replaced.
  if ((isset($context['tokens']['url:absolute']) && empty($replacements[$context['tokens']['url:absolute']])) &&
    (array_key_exists('user', $context['data']) && $context['data']['user'] === NULL)) {
    $replacements[$context['tokens']['url:absolute']] = NULL;
  }
}

/**
 * Implements hook_social_user_account_header_account_links().
 *
 * Adds the "View my profile" and "Edit profile" link to the user menu.
 */
function social_user_social_user_account_header_account_links(array $context) {
  // We require a user for these links.
  if (empty($context['user']) || !($context['user'] instanceof AccountInterface)) {
    return [];
  }

  $my_account_link = Link::createFromRoute((new TranslatableMarkup('Settings')), 'entity.user.edit_form', [
    'user' => $context['user']->id(),
  ]);
  $logout_link = Link::createFromRoute((new TranslatableMarkup('Logout')), 'user.logout');

  return [
    'my_account' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup("Settings"),
      ],
      '#weight' => 1200,
      '#access' => $my_account_link->getUrl()->access(),
    ] + $my_account_link->toRenderable(),
    'divider_logout' => [
      "#wrapper_attributes" => [
        "class" => ["divider"],
        "role" => "separator",
      ],
      '#weight' => 1400,
    ],
    'logout' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup("Logout"),
      ],
      '#weight' => 1500,
      '#access' => $logout_link->getUrl()->access(),
    ] + $logout_link->toRenderable(),
  ];
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Exposes the signup/login help texts to the site administrator.
 */
function social_user_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) {
  $config = \Drupal::config('social_user.settings');

  $fieldset = social_user_ensure_help_text_fieldset($form);

  $form[$fieldset]['signup_help'] = [
    '#type' => 'textarea',
    '#title' => new TranslatableMarkup('Sign Up'),
    '#description' => new TranslatableMarkup("Displayed in the user sign-up card."),
    "#default_value" => $config->get('signup_help'),
    '#weight' => 10,
  ];
  $form[$fieldset]['login_help'] = [
    '#type' => 'textarea',
    '#title' => new TranslatableMarkup('Log In'),
    '#description' => new TranslatableMarkup("Displayed in the user log in card."),
    "#default_value" => $config->get('login_help'),
    '#weight' => 20,
  ];

  $form['registration_cancellation']['verified_immediately'] = [
    '#type' => 'checkbox',
    '#title' => new TranslatableMarkup('Registered users are verified immediately'),
    '#description' => new TranslatableMarkup('New registered users get the "verified user & authenticated" role when they are registered. With this setting disabled, new registered users get the "authenticated" role when they are registered.'),
    '#default_value' => $config->get('verified_immediately'),
    '#weight' => -10,
  ];

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

/**
 * Stores the help texts for the social_user module.
 */
function social_user_form_user_admin_settings_submit($form, FormStateInterface $form_state) {
  $config = \Drupal::configFactory()
    ->getEditable('social_user.settings');
  $config->set('signup_help', $form_state->getValue("signup_help"));
  $config->set('login_help', $form_state->getValue("login_help"));
  $config->set('verified_immediately', $form_state->getValue("verified_immediately"));
  $config->save();
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Alter Basic site settings form.
 */
function social_user_form_system_site_information_settings_alter(&$form, FormStateInterface $form_state) {
  $config = \Drupal::config('social_user.settings');

  $form['site_information']['show_mail_in_messages'] = [
    '#type' => 'checkbox',
    '#title' => new TranslatableMarkup('Show email address in help messages'),
    '#description' => new TranslatableMarkup("Show site email address in help messages after failed login, signup or password reset."),
    "#default_value" => $config->get('show_mail_in_messages'),
    '#weight' => 10,
  ];

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

/**
 * Stores Show email address in help messages variable.
 */
function social_user_form_system_site_information_settings_submit($form, FormStateInterface $form_state) {
  $config = \Drupal::configFactory()
    ->getEditable('social_user.settings');
  $config->set('show_mail_in_messages', (boolean) $form_state->getValue("show_mail_in_messages"));
  $config->save();
}

/**
 * Ensures that the form contains a `user_help_texts` fieldset.
 *
 * The fieldset is targetted to be positioned after the
 * registration_cancellation element of the account settings form. If the
 * target can't be found then the fieldset is appended to the end. If the
 * fieldset has already been added then nothing happens.
 *
 * @return string
 *   The key of the fieldset that was added.
 */
function social_user_ensure_help_text_fieldset(&$form) {
  $key = 'user_help_texts';

  if (in_array($key, array_keys($form))) {
    return $key;
  }

  $fieldset = [
    '#type' => 'details',
    '#open' => TRUE,
    '#title' => new TranslatableMarkup("Login and Registration help texts"),
    '#description' => new TranslatableMarkup("These fields allow you to configure various help texts that are shown to users during signup and login"),
  ];

  // Insert the fieldset after the registration and cancellation settings.
  $index = array_search("registration_cancellation", array_keys($form));
  if ($index !== FALSE && $index !== count($form)) {
    $form = array_slice($form, 0, $index + 1, TRUE) +
      [$key => $fieldset] +
      array_slice($form, $index + 1, NULL, TRUE);
  }
  else {
    // Fallback to end of array appending if we can't find our target key.
    $form[$key] = $fieldset;
  }

  return $key;
}

/**
 * Implements hook_user_format_name_alter().
 *
 * Gathers the suggestions for a user display name from other module and
 * displays the one with the lowest weight (highest priority).
 */
function social_user_user_format_name_alter(&$name, AccountInterface $account) {
  $suggested_names = &drupal_static(__FUNCTION__, []);

  if (!isset($suggested_names[$account->id()])) {
    // We always add the username as fallback suggestion.
    $suggestions = [
      'username' => [
        'name' => $account->getAccountName(),
        'weight' => PHP_INT_MAX,
      ],
    ];

    $suggestions += \Drupal::moduleHandler()->invokeAll('social_user_name_display_suggestions', [$account]);
    \Drupal::moduleHandler()->alter('social_user_name_display_suggestions', $suggestions, $account);

    // We could use -PHP_INT_MAX but someone being clever might shoot
    // themselves.
    $lowest_weight = NULL;

    // If our array ends up being empty then we just stick to Drupal's default.
    foreach ($suggestions as $suggestion) {
      // Allow weight to be omitted and default to 0.
      $suggestion['weight'] = $suggestion['weight'] ?? 0;

      // If the suggestion's weight is equal to a previous entry then we use the
      // item that appears first in the array.
      if ($lowest_weight === NULL || $suggestion['weight'] < $lowest_weight) {
        $suggested_names[$account->id()] = $suggestion['name'];
        $lowest_weight = $suggestion['weight'];
      }
    }
  }

  $name = $suggested_names[$account->id()];
}

/**
 * Implements hook_views_pre_render().
 */
function social_user_views_pre_render(ViewExecutable $view) {
  if ($view->storage->id() == 'user_admin_people') {
    $view->element['#attached']['library'][] = 'social_user/admin.people';
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function social_user_module_implements_alter(&$implementations, $hook) {
  if (isset($implementations['social_user']) &&
    ($hook === 'form_user_form_alter' || $hook === 'form_alter')) {
    $social_user = $implementations['social_user'];
    unset($implementations['social_user']);
    $implementations['social_user'] = $social_user;
  }
}

/**
 * Implements hook_ENTITY_TYPE_insert().
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 */
function social_user_user_insert(UserInterface $user): void {
  // If site setting "Registered users are verified immediately" is enabled:
  // new registered users get the "verified user & authenticated" role when they
  // are registered.
  $config = \Drupal::config('social_user.settings');
  $verified_immediately = $config->get('verified_immediately');
  if ($verified_immediately) {
    $user->addRole('verified');
    $user->save();
  }
}

/**
 * Implements hook_views_data_alter().
 */
function social_user_views_data_alter(array &$data): void {
  if (isset($data['groups_field_data'])) {
    $filter = &$data['groups_field_data']['id']['filter'];
    $filter['id'] = 'in_operator';

    $filter['options callback'] = [
      SocialGroupHelperService::class,
      'getSplitGroups',
    ];
  }
}

/**
 * Implements hook_preprocess_HOOK().
 *
 * Override the default drupal truncate function for all usernames, so the whole
 * name will be displayed.
 */
function social_user_preprocess_username(array &$variables): void {
  $account = $variables['account'] ?: new AnonymousUserSession();
  $variables['name'] = $account->getDisplayName();
  $variables['truncated'] = FALSE;
}
