<?php

namespace Drupal\nodehive_core_beekeeper;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\Core\Serialization\Yaml as SerializationYaml;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Component\Yaml\Yaml;

/**
 * Beekeeper CLI tool.
 */
class Beekeeper implements BeekeeperInterface {

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

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The permission handler.
   *
   * @var \Drupal\user\PermissionHandlerInterface
   */
  protected PermissionHandlerInterface $permissionHandler;

  /**
   * Beekeeper constructor.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module_handler service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
   *   The config factory.
   * @param \Drupal\user\PermissionHandlerInterface $permission_handler
   *   The permission handler.
   */
  public function __construct(
    ModuleHandlerInterface $moduleHandler,
    FileSystemInterface $file_system,
    EntityTypeManagerInterface $entity_type_manager,
    ConfigFactoryInterface $config,
    PermissionHandlerInterface $permission_handler,
  ) {
    $this->moduleHandler = $moduleHandler;
    $this->fileSystem = $file_system;
    $this->entityTypeManager = $entity_type_manager;
    $this->configFactory = $config;
    $this->permissionHandler = $permission_handler;
  }

  /**
   * {@inheritdoc}
   */
  public function executeDrushCommands(string $preset): void {
    if (PHP_SAPI !== 'cli') {
      throw new \Exception("This method needs to be run in CLI context.");
    }

    $config = $this->scanConfig($preset);
    if (!isset($config[$preset]) || !isset($config[$preset]['drush'])) {
      echo "No drush config found for preset $preset.";
      return;
    }

    // Get drush path.
    $drush_path = dirname(DRUPAL_ROOT) . '/vendor/bin/drush';

    echo "Starting to execute drush commands for $preset preset";
    foreach ($config[$preset]['drush'] as $cmd => $args) {
      foreach ($args as $arg) {
        $full_cmd = [$drush_path, $cmd];
        $arg = preg_split("/'[^']*'(*SKIP)(*F)|\x20/", $arg);

        // Remove ' from args.
        foreach ($arg as $key => $value) {
          $arg[$key] = str_replace("'", "", $value);
        }

        $full_cmd = array_merge($full_cmd, $arg);
        $this->executeExternalCmd($full_cmd);
      }
    }
    $this->executeExternalCmd([$drush_path, "cr"]);

    echo "Finished running drush commands.";
  }

  /**
   * {@inheritdoc}
   */
  public function installDrupalMenu(string $preset): void {
    if (PHP_SAPI !== 'cli') {
      throw new \Exception("This method needs to be run in CLI context.");
    }

    $config = $this->scanConfig($preset);
    if (!isset($config[$preset]) || !isset($config[$preset]['menu'])) {
      echo "No menu found for preset $preset.";
      return;
    }

    // Create menus.
    foreach ($config[$preset]['menu'] as $menu_id => $data) {
      $existing = $this->entityTypeManager
        ->getStorage('menu')
        ->load($menu_id);

      // Delete everything and recreate.
      if ($existing) {
        $links = $this->entityTypeManager->getStorage('menu_link_content')
          ->loadByProperties(['menu_name' => $existing->id()]);

        foreach ($links as $link) {
          $link->delete();
        }

        $existing->delete();
      }

      $this->entityTypeManager
        ->getStorage('menu')
        ->create([
          'id' => $menu_id,
          'label' => $data['label'],
          'description' => $data['description'],
        ])
        ->save();
    }

    foreach ($config[$preset]['menu'] as $menu_id => $data) {
      foreach ($data['links'] as $link) {
        $menu_link = $this->createMenuLink($menu_id, $link['title'], $link['link'], NULL, (int) $link['weight']);
        if (isset($link['children'])) {
          foreach ($link['children'] as $child) {
            $child = $this->createMenuLink($menu_id, $child['title'], $child['link'], $menu_link->uuid(), (int) $link['weight']);
          }
        }

      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function installDrupalPermissions(string $preset): void {
    if (PHP_SAPI !== 'cli') {
      throw new \Exception("This method needs to be run in CLI context.");
    }

    $config = $this->scanConfig($preset);

    if (!isset($config[$preset]) || !isset($config[$preset]['permission'])) {
      echo "No permissions found for preset $preset. Create your_module/config/beekeeper/$preset/beekeeper.permission.yml if you like to create your custom permission preset.";
      return;
    }

    // Iterate over roles.
    foreach ($config[$preset]['permission'] as $role_id => $data) {

      /** @var \Drupal\user\RoleInterface $role */
      $role = $this->entityTypeManager
        ->getStorage('user_role')
        ->load($role_id);

      // If role does not exist, create it.
      if (!$role) {
        /** @var \Drupal\user\RoleInterface $role */
        $role = $this->entityTypeManager
          ->getStorage("user_role")
          ->create([
            "id" => $role_id,
            "label" => $data["label"],
          ]);
      }

      // Iterate over permissions and create them.
      $permission_definitions = $this->permissionHandler->getPermissions();
      if (!empty($data['permissions']) && is_array($data['permissions'])) {
        $valid_permissions = array_intersect($data['permissions'], array_keys($permission_definitions));
        $invalid_permissions = array_diff($data['permissions'], $valid_permissions);

        foreach ($data['permissions'] as $permission) {
          if (in_array($permission, $invalid_permissions)) {
            echo "[WARNING] Invalid permission: $permission\r\n";
            continue;
          }

          $role->grantPermission($permission);
        }
      }

      $role->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function installDrupalConfig(string $preset): void {
    if (PHP_SAPI !== 'cli') {
      throw new \Exception("This method needs to be run in CLI context.");
    }

    $configs = $this->getDefaultConfig($preset);
    if (!isset($configs) || empty($configs)) {
      echo "No configs found for preset $preset.";
      return;
    }

    foreach ($configs as $preset_configs) {
      if (!$preset_configs || empty($preset_configs)) {
        continue;
      }

      foreach ($preset_configs as $config) {
        $name = $config->name;
        $path = $config->uri;

        $this->configFactory->getEditable($name)
          ->setData(Yaml::parseFile($path))
          ->save();
      }
    }
  }

  /**
   * Get default config for a preset.
   *
   * @param string $preset
   *   Preset name.
   */
  protected function getDefaultConfig(string $preset): ?array {
    $configs = [];

    $modules = $this->moduleHandler->getModuleList();
    foreach ($modules as $module) {
      $module_config_path = $module->getPath() . '/config/beekeeper/' . $preset . '/default';
      if (!is_dir($module_config_path)) {
        continue;
      }

      $config = $this->fileSystem
        ->scanDirectory(DRUPAL_ROOT . '/' . $module_config_path, '/.*\.yml$/');

      $configs[$module->getName()] = $config;
    }

    return $configs;
  }

  /**
   * Executes an external command via Symfony process class.
   *
   * If cmds fails, then an exception is thrown.
   *
   * @param array $cmd
   *   Array with commands and arguments that will get executed.
   */
  protected function executeExternalCmd(array $cmd): void {
    $process = new Process($cmd);
    $process->setTimeout(300);
    $process->setWorkingDirectory(dirname(DRUPAL_ROOT, 1));
    $process->run(function ($type, $buffer) {
      echo $buffer;
      flush();
      ob_flush();
    });

    if (!$process->isSuccessful()) {
      throw new ProcessFailedException($process);
    }
  }

  /**
   * Scans the module directories and searches for beekeeper configs.
   *
   * @param string $preset
   *   Preset name.
   *
   * @return array
   *   Array with keys as preset type.
   */
  protected function scanConfig(string $preset): array {
    $config = [];

    $modules = $this->moduleHandler->getModuleList();
    $beekeeper_configs = [];
    foreach ($modules as $module) {
      $module_config_path = $module->getPath() . '/config/beekeeper/' . $preset;
      if (!is_dir($module_config_path)) {
        continue;
      }

      $beekeeper_config = $this->fileSystem
        ->scanDirectory(DRUPAL_ROOT . '/' . $module_config_path, '/beekeeper\..*/');
      $beekeeper_configs[] = $beekeeper_config;
    }

    if (!$beekeeper_configs) {
      return $config;
    }

    foreach ($beekeeper_configs as $beekeeper_config) {
      foreach ($beekeeper_config as $config_file) {
        $explode = explode(".", $config_file->name);
        $install_type = $explode[1];

        $config_yaml = SerializationYaml::decode(file_get_contents($config_file->uri));
        $config[$preset][$install_type] = $config_yaml;
      }
    }

    return $config;
  }

  /**
   * Creates a new menu content link.
   *
   * @param string $menu_name
   *   Menu name.
   * @param string $title
   *   Link title.
   * @param string $uri
   *   Link uri.
   * @param string $parent_uuid
   *   Parent uuid.
   * @param int $weight
   *   Link weight.
   *
   * @return \Drupal\menu_link_content\MenuLinkContentInterface
   *   A new menu link content.
   */
  protected function createMenuLink(string $menu_name, string $title, string $uri, ?string $parent_uuid = NULL, ?int $weight = NULL) {
    $menu_link = $this->entityTypeManager
      ->getStorage('menu_link_content')
      ->loadByProperties([
        'title' => $title,
        'menu_name' => $menu_name,
      ]);

    if (empty($menu_link)) {
      $data = [
        'title' => $title,
        'link' => [
          'uri' => $uri,
        ],
        'menu_name' => $menu_name,
      ];

      if ($parent_uuid) {
        $data['parent'] = "menu_link_content:$parent_uuid";
      }

      if ($weight) {
        $data['weight'] = $weight;
      }

      $menu_link = MenuLinkContent::create($data);
      $menu_link->save();
    }
    else {
      $menu_link = end($menu_link);
    }

    return $menu_link;
  }

}
