<?php

declare(strict_types=1);

namespace Drupal\nodehive_path_alias\Hook;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\node\NodeInterface;

/**
 * Hook implementations for node edit form enhancements.
 */
final class NodeFormHooks {

  /**
   * Constructs a NodeFormHooks instance.
   */
  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly MessengerInterface $messenger,
  ) {}

  /**
   * Adds a read-only NodeHive alias scope prototype to node edit forms.
   */
  public function formNodeFormAlter(array &$form, FormStateInterface $formState, string $formId): void {
    $entity = $formState->getFormObject()->getEntity();
    if (!$entity instanceof NodeInterface || !$entity->hasField('nodehive_space')) {
      return;
    }

    if (!isset($form['path'])) {
      return;
    }

    if (isset($form['actions']['submit'])) {
      if (!in_array('nodehive_path_alias_node_form_validate', $form['actions']['submit']['#validate'] ?? [], TRUE)) {
        $form['actions']['submit']['#validate'][] = 'nodehive_path_alias_node_form_validate';
      }
      if (!in_array('nodehive_path_alias_node_form_submit', $form['actions']['submit']['#submit'] ?? [], TRUE)) {
        $form['actions']['submit']['#submit'][] = 'nodehive_path_alias_node_form_submit';
      }
    }

    $selectedSpaceIds = $this->getSelectedSpaceIds($form, $entity);
    $prototype = $this->buildPrototype($entity, $selectedSpaceIds);

    if (isset($form['path']['widget'][0]) && is_array($form['path']['widget'][0])) {
      $form['path']['widget'][0]['nodehive_alias_scope_prototype'] = $prototype;
      return;
    }

    if (isset($form['path']['widget']) && is_array($form['path']['widget'])) {
      $form['path']['widget']['nodehive_alias_scope_prototype'] = $prototype;
      return;
    }

    $form['path']['nodehive_alias_scope_prototype'] = $prototype;
  }

  /**
   * Builds the render array for the prototype UI.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity being edited.
   * @param int[] $selectedSpaceIds
   *   The selected NodeHive space IDs.
   */
  private function buildPrototype(NodeInterface $node, array $selectedSpaceIds): array {
    $spaceLabels = $this->loadSpaceLabels($selectedSpaceIds);
    $savedAliases = $this->loadSavedAliases($node);

    $build = [
      '#type' => 'container',
      '#weight' => 90,
      '#attributes' => [
        'class' => ['nodehive-path-alias-prototype'],
      ],
      'title' => [
        '#markup' => '<div class="form-item__description"><strong>' . Html::escape((string) $this->t('NodeHive alias scope prototype')) . '</strong></div>',
      ],
      'summary' => [
        '#markup' => '<div class="form-item__description">' . Html::escape($this->buildSummary($selectedSpaceIds, $node->isNew())) . '</div>',
      ],
    ];

    if ($selectedSpaceIds !== []) {
      $build['space_aliases'] = [
        '#type' => 'container',
      ];

      foreach ($selectedSpaceIds as $spaceId) {
        $build['space_aliases'][(string) $spaceId] = [
          '#type' => 'textarea',
          '#title' => $this->t('Aliases for %space', [
            '%space' => $spaceLabels[$spaceId] ?? (string) $spaceId,
          ]),
          '#description' => $this->t('Prototype input. Add one alias per line for this space only.'),
          '#rows' => 3,
          '#parents' => ['nodehive_path_alias_prototype', 'space_aliases', (string) $spaceId],
        ];
      }
    }
    else {
      $build['global_aliases'] = [
        '#type' => 'textarea',
        '#title' => $this->t('Global aliases'),
        '#description' => $this->t('No NodeHive space is selected. Add one alias per line to create global aliases.'),
        '#rows' => 3,
        '#parents' => ['nodehive_path_alias_prototype', 'global_aliases'],
      ];
    }

    if ($spaceLabels !== []) {
      $build['selected_spaces'] = [
        '#theme' => 'item_list',
        '#title' => $this->t('Selected spaces'),
        '#items' => $spaceLabels,
      ];
    }

    if ($savedAliases !== []) {
      $build['saved_aliases'] = [
        '#type' => 'table',
        '#caption' => $this->t('Saved alias rows for this node'),
        '#header' => [
          $this->t('Alias'),
          $this->t('Language'),
          $this->t('Scope'),
        ],
        '#rows' => $savedAliases,
        '#empty' => $this->t('No saved aliases yet.'),
      ];
    }
    elseif (!$node->isNew()) {
      $build['saved_alias_note'] = [
        '#markup' => '<div class="form-item__description">' . Html::escape((string) $this->t('No saved path alias rows exist for this node yet.')) . '</div>',
      ];
    }

    return $build;
  }

  /**
   * Validates additional aliases entered through the prototype UI.
   */
  public function validateNodeForm(array &$form, FormStateInterface $formState): void {
    $aliasesByScope = $this->getSubmittedAliasesByScope($formState);
    if ($aliasesByScope === []) {
      return;
    }

    $entity = $formState->getFormObject()->buildEntity($form, $formState);
    if (!$entity instanceof NodeInterface) {
      return;
    }

    $langcode = $entity->language()->getId();
    $internalPath = $entity->isNew() ? NULL : '/' . $entity->toUrl()->getInternalPath();
    $spaceIds = array_values(array_filter(array_map(static fn (string $scope): int => ctype_digit($scope) ? (int) $scope : 0, array_keys($aliasesByScope))));
    $spaceLabels = $this->loadSpaceLabels($spaceIds);

    foreach ($aliasesByScope as $scopeKey => $aliases) {
      $scopeId = $scopeKey === 'global' ? NULL : (int) $scopeKey;

      foreach ($aliases as $alias) {
        if ($internalPath !== NULL && $this->aliasExistsForPath($internalPath, $alias, $langcode, $scopeId)) {
          continue;
        }

        if ($this->aliasConflicts($alias, $langcode, $scopeId, $internalPath)) {
          $scopeLabel = $scopeId !== NULL
            ? ($spaceLabels[$scopeId] ?? (string) $scopeId)
            : (string) $this->t('global scope');
          $formState->setErrorByName(
            $this->buildScopeErrorName($scopeKey),
            (string) $this->t('The alias %alias is already in use for %scope.', [
              '%alias' => $alias,
              '%scope' => $scopeLabel,
            ]),
          );

          return;
        }
      }
    }
  }

  /**
   * Creates any additional aliases after the node save succeeds.
   */
  public function submitNodeForm(array &$form, FormStateInterface $formState): void {
    $aliasesByScope = $this->getSubmittedAliasesByScope($formState);
    if ($aliasesByScope === []) {
      return;
    }

    $entity = $formState->getFormObject()->getEntity();
    if (!$entity instanceof NodeInterface || $entity->isNew()) {
      return;
    }

    $internalPath = '/' . $entity->toUrl()->getInternalPath();
    $langcode = $entity->language()->getId();
    $storage = $this->entityTypeManager->getStorage('path_alias');
    $createdCount = 0;

    foreach ($aliasesByScope as $scopeKey => $aliases) {
      $scopeId = $scopeKey === 'global' ? NULL : (int) $scopeKey;

      foreach ($aliases as $alias) {
        if ($this->aliasExistsForPath($internalPath, $alias, $langcode, $scopeId)) {
          continue;
        }

        $values = [
          'path' => $internalPath,
          'alias' => $alias,
          'langcode' => $langcode,
        ];

        if ($scopeId !== NULL) {
          $values['nodehive_space'] = $scopeId;
        }

        /** @var \Drupal\path_alias\PathAliasInterface $pathAlias */
        $pathAlias = $storage->create($values);
        $pathAlias->save();
        $createdCount++;
      }
    }

    if ($createdCount > 0) {
      $this->messenger->addStatus((string) $this->t('Created @count additional alias rows.', [
        '@count' => $createdCount,
      ]));
    }
  }

  /**
   * Returns the selected NodeHive space IDs from the form or entity.
   *
   * @return int[]
   *   The selected space IDs.
   */
  private function getSelectedSpaceIds(array $form, NodeInterface $node): array {
    $candidates = [
      $form['nodehive_space_information'][0]['widget']['#default_value'] ?? NULL,
      $form['nodehive_space']['widget']['#default_value'] ?? NULL,
    ];

    foreach ($candidates as $candidate) {
      if (!is_array($candidate) || $candidate === []) {
        continue;
      }

      $ids = array_values(array_filter(array_map(static function (mixed $value): int {
        return is_scalar($value) ? (int) $value : 0;
      }, $candidate)));

      if ($ids !== []) {
        return $ids;
      }
    }

    $ids = [];
    foreach ($node->get('nodehive_space') as $item) {
      if (!empty($item->target_id)) {
        $ids[] = (int) $item->target_id;
      }
    }

    return $ids;
  }

  /**
   * Loads labels for the selected spaces.
   *
   * @param int[] $spaceIds
   *   The selected space IDs.
   *
   * @return array<int, string>
   *   Human-readable space labels keyed by space ID.
   */
  private function loadSpaceLabels(array $spaceIds): array {
    if ($spaceIds === []) {
      return [];
    }

    $spaces = $this->entityTypeManager
      ->getStorage('nodehive_space')
      ->loadMultiple($spaceIds);

    $labels = [];
    foreach ($spaceIds as $spaceId) {
      if (isset($spaces[$spaceId])) {
        $labels[$spaceId] = $spaces[$spaceId]->label();
      }
    }

    return $labels;
  }

  /**
   * Loads existing path alias rows for the current node path.
   *
   * @return array<int, array<int, string>>
   *   Table rows keyed numerically.
   */
  private function loadSavedAliases(NodeInterface $node): array {
    if ($node->isNew()) {
      return [];
    }

    $internalPath = '/' . $node->toUrl()->getInternalPath();
    $storage = $this->entityTypeManager->getStorage('path_alias');

    $ids = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('path', $internalPath)
      ->sort('id')
      ->execute();

    if ($ids === []) {
      return [];
    }

    $aliases = $storage->loadMultiple($ids);
    $spaceIds = [];
    foreach ($aliases as $alias) {
      $spaceId = $alias->get('nodehive_space')->target_id ?? NULL;
      if ($spaceId !== NULL) {
        $spaceIds[] = (int) $spaceId;
      }
    }

    $spaceLabelsById = $this->loadSpaceLabels(array_values(array_unique($spaceIds)));

    $rows = [];
    foreach ($aliases as $alias) {
      $spaceId = $alias->get('nodehive_space')->target_id ?? NULL;
      $rows[] = [
        $alias->getAlias(),
        $alias->language()->getId(),
        $spaceId !== NULL ? ($spaceLabelsById[(int) $spaceId] ?? (string) $spaceId) : (string) $this->t('Global'),
      ];
    }

    return $rows;
  }

  /**
   * Returns normalized aliases from the submitted textarea.
   *
   * @return string[]
   *   Unique aliases in submitted order.
   */
  private function getSubmittedAliasesByScope(FormStateInterface $formState): array {
    $aliasesByScope = [];

    $perSpace = $formState->getValue(['nodehive_path_alias_prototype', 'space_aliases']);
    if (is_array($perSpace)) {
      foreach ($perSpace as $scopeKey => $raw) {
        $aliases = $this->normalizeAliasList($raw);
        if ($aliases !== []) {
          $aliasesByScope[(string) $scopeKey] = $aliases;
        }
      }
    }

    $global = $formState->getValue(['nodehive_path_alias_prototype', 'global_aliases']);
    $globalAliases = $this->normalizeAliasList($global);
    if ($globalAliases !== []) {
      $aliasesByScope['global'] = $globalAliases;
    }

    return $aliasesByScope;
  }

  /**
   * Normalizes a submitted alias string.
   */
  private function normalizeAlias(string $alias): ?string {
    $alias = trim($alias);
    $alias = rtrim($alias, " \\/");

    if ($alias === '') {
      return NULL;
    }

    if (!str_starts_with($alias, '/')) {
      $alias = '/' . $alias;
    }

    return $alias;
  }

  /**
   * Normalizes aliases from a textarea value.
   *
   * @return string[]
   *   Unique aliases in submitted order.
   */
  private function normalizeAliasList(mixed $raw): array {
    if (!is_string($raw) || trim($raw) === '') {
      return [];
    }

    $aliases = [];
    foreach (preg_split('/\R+/', $raw) ?: [] as $line) {
      $alias = $this->normalizeAlias($line);
      if ($alias !== NULL) {
        $aliases[$alias] = $alias;
      }
    }

    return array_values($aliases);
  }

  /**
   * Returns whether the same alias row already exists for this path and scope.
   */
  private function aliasExistsForPath(string $path, string $alias, string $langcode, ?int $spaceId): bool {
    $storage = $this->entityTypeManager->getStorage('path_alias');
    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('path', $path)
      ->condition('alias', $alias)
      ->condition('langcode', $langcode);

    $this->applyScopeCondition($query, $spaceId);

    return $query->range(0, 1)->count()->execute() > 0;
  }

  /**
   * Returns whether the alias conflicts with another path in the same scope.
   */
  private function aliasConflicts(string $alias, string $langcode, ?int $spaceId, ?string $currentPath): bool {
    $storage = $this->entityTypeManager->getStorage('path_alias');
    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('alias', $alias)
      ->condition('langcode', $langcode);

    if ($currentPath !== NULL) {
      $query->condition('path', $currentPath, '<>');
    }

    $this->applyScopeCondition($query, $spaceId);

    return $query->range(0, 1)->count()->execute() > 0;
  }

  /**
   * Applies the per-space scope condition to a path alias entity query.
   */
  private function applyScopeCondition(QueryInterface $query, ?int $spaceId): void {
    if ($spaceId !== NULL) {
      $query->condition('nodehive_space', $spaceId);
      return;
    }

    $query->notExists('nodehive_space');
  }

  /**
   * Builds the form error name for a given alias scope textarea.
   */
  private function buildScopeErrorName(string $scopeKey): string {
    if ($scopeKey === 'global') {
      return 'nodehive_path_alias_prototype][global_aliases';
    }

    return 'nodehive_path_alias_prototype][space_aliases][' . $scopeKey;
  }

  /**
   * Builds the summary line shown above the prototype.
   *
   * @param int[] $selectedSpaceIds
   *   The selected space IDs.
   * @param bool $isNew
   *   Whether the node is new (unsaved).
   */
  private function buildSummary(array $selectedSpaceIds, bool $isNew): string {
    if ($selectedSpaceIds === []) {
      return (string) $this->t('No NodeHive space is selected, so a manual alias will be treated as global.');
    }

    if (count($selectedSpaceIds) === 1) {
      return (string) $this->t('A manual alias on this node will be scoped to the selected NodeHive space.');
    }

    $message = (string) $this->t('A manual alias on this node will be duplicated once per selected NodeHive space.');
    if ($isNew) {
      $message .= ' ' . (string) $this->t('Saved alias rows will appear here after the first save.');
    }

    return $message;
  }

  /**
   * Shorthand for string translation.
   */
  private function t(string $string, array $args = []): TranslatableMarkup {
    return new TranslatableMarkup($string, $args); // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
  }

}
