<?php

namespace Drupal\package_manager\Validator;

use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\Finder\Finder;

/**
 * Flags errors if the project root or staging area contain symbolic links.
 *
 * @todo Remove this when Composer Stager's PHP file copier handles symlinks
 *   without issues.
 *
 * @internal
 *   This is an internal part of Package Manager and may be changed or removed
 *   at any time without warning. External code should not interact with this
 *   class.
 */
class SymlinkValidator implements PreOperationStageValidatorInterface {

  use StringTranslationTrait;

  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The path locator service.
   *
   * @var \Drupal\package_manager\PathLocator
   */
  protected $pathLocator;

  /**
   * Constructs a SymlinkValidator object.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler service.
   * @param \Drupal\package_manager\PathLocator $path_locator
   *   The path locator service.
   */
  public function __construct(ModuleHandlerInterface $module_handler, PathLocator $path_locator) {
    $this->moduleHandler = $module_handler;
    $this->pathLocator = $path_locator;
  }

  /**
   * {@inheritdoc}
   */
  public function validateStagePreOperation(PreOperationStageEvent $event): void {
    $dir = $this->pathLocator->getProjectRoot();

    if ($this->hasLinks($dir)) {
      $this->addError('Symbolic links were found in the active directory, which are not supported at this time.', $event);
    }
  }

  /**
   * Checks if the staging area has any symbolic links.
   *
   * @param \Drupal\package_manager\Event\PreApplyEvent $event
   *   The event object.
   */
  public function preApply(PreApplyEvent $event): void {
    $dir = $event->getStage()->getStageDirectory();

    if ($this->hasLinks($dir)) {
      $this->addError('Symbolic links were found in the staging area, which are not supported at this time.', $event);
    }
  }

  /**
   * Recursively checks if a directory has any symbolic links.
   *
   * @param string $dir
   *   The path of the directory to check.
   *
   * @return bool
   *   TRUE if the directory contains any symbolic links, FALSE otherwise.
   */
  protected function hasLinks(string $dir): bool {
    // Finder::filter() explicitly requires a closure, so create one from
    // ::isLink() so that we can still override it for testing purposes.
    $is_link = \Closure::fromCallable([$this, 'isLink']);

    // Finder::hasResults() is more efficient than count() because it will
    // return early if there is a match.
    return Finder::create()
      ->in($dir)
      ->filter($is_link)
      ->ignoreUnreadableDirs()
      ->hasResults();
  }

  /**
   * Checks if a file or directory is a symbolic link.
   *
   * @param \SplFileInfo $file
   *   A value object for the file or directory.
   *
   * @return bool
   *   TRUE if the file or directory is a symbolic link, FALSE otherwise.
   */
  protected function isLink(\SplFileInfo $file): bool {
    return $file->isLink();
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      PreCreateEvent::class => 'validateStagePreOperation',
      StatusCheckEvent::class => 'validateStagePreOperation',
      PreApplyEvent::class => [
        ['validateStagePreOperation'],
        ['preApply'],
      ],
    ];
  }

  /**
   * Adds a validation error to a given event.
   *
   * @param string $message
   *   The error message. If the Help module is enabled, a link to Package
   *   Manager's help page will be appended.
   * @param \Drupal\package_manager\Event\PreApplyEvent|\Drupal\package_manager\Event\PreOperationStageEvent $event
   *   The event to add the error to.
   *
   * @see package_manager_help()
   */
  protected function addError(string $message, $event): void {
    if ($this->moduleHandler->moduleExists('help')) {
      $url = Url::fromRoute('help.page', ['name' => 'package_manager'])
        ->setOption('fragment', 'package-manager-faq-symlinks-found')
        ->toString();

      $message = $this->t('@message See <a href=":package-manager-help">the help page</a> for information on how to resolve the problem.', [
        '@message' => $message,
        ':package-manager-help' => $url,
      ]);
    }

    $event->addError([$message]);
  }

}