<?php

declare(strict_types=1);

namespace Drupal\nodehive_path_alias\Hook;

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\nodehive_path_alias\Plugin\Validation\Constraint\NodehiveSpaceUniquePathAliasConstraint;
use Drupal\path_alias\PathAliasInterface;

/**
 * Hook implementations for nodehive_path_alias.
 */
final class PathAliasHooks {

  /**
   * Stash of space IDs per path_alias object, keyed by spl_object_id.
   *
   * Used to pass data from pathAliasPresave() to pathAliasInsert() without
   * relying on dynamic properties (not allowed on interfaces).
   *
   * @var array<int, int[]>
   */
  private array $pendingSpaces = [];

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

  /**
   * Adds the nodehive_space base field to the path_alias entity type.
   */
  public function entityBaseFieldInfo(EntityTypeInterface $entityType): array {
    $fields = [];
    if ($entityType->id() === 'path_alias') {
      $fields['nodehive_space'] = BaseFieldDefinition::create('entity_reference')
        ->setLabel(new TranslatableMarkup('NodeHive Space'))
        ->setDescription(new TranslatableMarkup('The NodeHive space this alias belongs to. NULL means a global alias.'))
        ->setSetting('target_type', 'nodehive_space')
        ->setCardinality(1)
        ->setRequired(FALSE)
        ->setDisplayOptions('form', [
          'type' => 'entity_reference_autocomplete',
          'weight' => 10,
          'settings' => [
            'match_operator' => 'CONTAINS',
            'size' => 60,
            'placeholder' => '',
          ],
        ])
        ->setDisplayConfigurable('view', FALSE)
        ->setDisplayConfigurable('form', TRUE);
    }

    return $fields;
  }

  /**
   * Replaces the UniquePathAlias constraint with the space-scoped version.
   */
  public function validationConstraintAlter(array &$definitions): void {
    $definitions['UniquePathAlias']['class'] = NodehiveSpaceUniquePathAliasConstraint::class;
  }

  /**
   * Assigns nodehive_space on new aliases derived from content entities.
   *
   * When the entity belongs to multiple spaces, the first space is assigned
   * here. The remaining space IDs are stashed on the alias object so that
   * pathAliasInsert() can create one clone per additional space.
   */
  public function pathAliasPresave(PathAliasInterface $pathAlias): void {
    // Skip aliases that already have a space (e.g. clones created below, or
    // manually managed aliases).
    if (!$pathAlias->get('nodehive_space')->isEmpty()) {
      return;
    }

    $entity = $this->resolveEntityFromPath($pathAlias->getPath());
    if ($entity === NULL) {
      return;
    }

    $spaceIds = $this->getSpaceIds($entity);
    if (empty($spaceIds)) {
      return;
    }

    // Assign the first space to this alias.
    $pathAlias->set('nodehive_space', array_shift($spaceIds));

    // Stash remaining space IDs for pathAliasInsert() to clone.
    if (!empty($spaceIds)) {
      $this->pendingSpaces[spl_object_id($pathAlias)] = $spaceIds;
    }
  }

  /**
   * Creates one per-space alias clone for each remaining space ID.
   *
   * Runs after the original alias is saved so it already has a persistent ID.
   * Each clone goes through pathAliasPresave(), which returns early because
   * nodehive_space is already set, preventing recursion.
   */
  public function pathAliasInsert(PathAliasInterface $pathAlias): void {
    $oid = spl_object_id($pathAlias);
    if (empty($this->pendingSpaces[$oid])) {
      return;
    }

    $spaceIds = $this->pendingSpaces[$oid];
    unset($this->pendingSpaces[$oid]);

    foreach ($spaceIds as $spaceId) {
      $clone = $pathAlias->createDuplicate();
      $clone->set('nodehive_space', $spaceId);
      $clone->save();
    }
  }

  /**
   * Resolves the content entity for a given path alias system path.
   *
   * Checks entity types with a canonical link template by converting the
   * template to a regex. Matches /node/{id}, /taxonomy/term/{id}, etc.
   */
  private function resolveEntityFromPath(string $path): ?FieldableEntityInterface {
    foreach ($this->entityTypeManager->getDefinitions() as $entityTypeId => $entityType) {
      if (!$entityType->hasLinkTemplate('canonical')) {
        continue;
      }

      $template = $entityType->getLinkTemplate('canonical');
      // Replace {param_name} placeholders with a numeric capture group.
      $regex = '#^' . preg_replace('/\{[^}]+\}/', '(\d+)', $template) . '$#';

      if (!preg_match($regex, $path, $matches)) {
        continue;
      }

      $entity = $this->entityTypeManager
        ->getStorage($entityTypeId)
        ->load((int) $matches[1]);

      if ($entity instanceof FieldableEntityInterface && $entity->hasField('nodehive_space')) {
        return $entity;
      }
    }

    return NULL;
  }

  /**
   * Returns all space IDs from an entity's nodehive_space field.
   *
   * @return int[]
   *   The space IDs referenced by the entity's nodehive_space field.
   */
  private function getSpaceIds(FieldableEntityInterface $entity): array {
    $ids = [];
    foreach ($entity->get('nodehive_space') as $item) {
      if (!empty($item->target_id)) {
        $ids[] = (int) $item->target_id;
      }
    }

    return $ids;
  }

}
