<?php

namespace Drupal\nodehive_content\Controller;

use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller for NodeHive Content admin page.
 */
class NodehiveContentController extends ControllerBase {

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

  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * The file URL generator service.
   *
   * @var \Drupal\Core\File\FileUrlGeneratorInterface
   */
  protected $fileUrlGenerator;

  /**
   * The content translation manager service.
   *
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
   */
  protected $contentTranslationManager;

  /**
   * Constructs a NodehiveContentController object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter service.
   * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
   *   The file URL generator service.
   * @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
   *   The content translation manager service.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, DateFormatterInterface $date_formatter, FileUrlGeneratorInterface $file_url_generator, ContentTranslationManagerInterface $content_translation_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->renderer = $renderer;
    $this->dateFormatter = $date_formatter;
    $this->fileUrlGenerator = $file_url_generator;
    $this->contentTranslationManager = $content_translation_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('renderer'),
      $container->get('date.formatter'),
      $container->get('file_url_generator'),
      $container->get('content_translation.manager')
    );
  }

  /**
   * Displays the content overview page.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return array
   *   A render array.
   */
  public function overview(Request $request) {
    $display_lang = $request->query->get('display_lang', '');

    // Determine if user can see all spaces.
    $current_user = $this->currentUser();
    $user_roles = $current_user->getRoles();
    $show_all_spaces = in_array('administrator', $user_roles)
      || in_array('nodehive_admin', $user_roles)
      || in_array('nodehive_owner', $user_roles)
      || (int) $current_user->id() === 1;

    // Load user-accessible spaces early so we can validate the selection.
    $spaces = $this->getSpaces();

    // If no space filter is set, preselect from the content browser cookie
    // (persists the user's last selection), then fall back to sidebar cookie.
    $space = $request->query->get('space');
    if ($space === NULL || $space === '') {
      $cookie_space = $request->cookies->get('nodehive_content_space')
        ?? $request->cookies->get('nodehive_sidebar_space');
      if ($cookie_space !== NULL && $cookie_space !== '') {
        $request->query->set('space', $cookie_space);
      }
    }

    $active_space = $request->query->get('space', '');
    $no_space = ($active_space === '__none__');
    $space_id = (!$no_space && $active_space !== NULL && $active_space !== '') ? (int) $active_space : NULL;

    // For non-admin users, ensure selected space is accessible and default
    // to the first accessible space if none is selected or invalid.
    if (!$show_all_spaces && !empty($spaces)) {
      if (!$no_space && ($space_id === NULL || !isset($spaces[$space_id]))) {
        $space_id = (int) array_key_first($spaces);
        $request->query->set('space', (string) $space_id);
      }
    }

    $result = $this->getNodes($request, $display_lang, $space_id);
    $node_types = $this->getNodeTypes();

    // Apply space filter to counts when a space is active.
    $count_conditions = [];
    if ($no_space) {
      $count_conditions['__no_space'] = TRUE;
    }
    elseif ($space_id !== NULL) {
      $count_conditions['nodehive_space'] = $space_id;
    }
    $node_type_counts = $this->getNodeTypeCounts($count_conditions);

    $languages = $this->getEnabledLanguages();
    $authors = $this->getContentAuthors();
    $space_content_types = $this->getSpaceContentTypes();

    // Build node-to-spaces mapping when viewing all spaces.
    $node_spaces = [];
    if ($space_id === NULL && !empty($result['nodes'])) {
      $node_spaces = $this->getNodeSpaces(array_column($result['nodes'], 'id'), $spaces);
    }

    $build = [
      '#theme' => 'nodehive_content_overview',
      '#nodes' => $result['nodes'],
      '#pager' => $result['pager'],
      '#node_types' => $node_types,
      '#node_type_counts' => $node_type_counts,
      '#languages' => $languages,
      '#authors' => $authors,
      '#spaces' => $spaces,
      '#space_groups' => $this->getSpaceGroups($spaces),
      '#node_spaces' => $node_spaces,
      '#space_content_types' => $space_content_types,
      '#show_all_spaces' => $show_all_spaces,
      '#hide_space_filter' => (bool) $request->attributes->get('hide_space_filter', FALSE),
      '#destination' => $request->getPathInfo(),
      '#filters' => [
        'title' => $request->query->get('title', ''),
        'type' => $request->query->get('type', ''),
        'status' => $request->query->get('status', ''),
        'author' => $request->query->get('author', ''),
        'space' => $request->query->get('space', ''),
        'display_lang' => $display_lang,
        'page' => $result['pager']['current_page'],
        'limit' => $result['pager']['items_per_page'],
      ],
      '#attached' => [
        'library' => [
          'nodehive_content/nodehive_content',
        ],
      ],
    ];

    return $build;
  }

  /**
   * Redirects to add content page with destination.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirect response.
   */
  public function addContent() {
    $url = Url::fromRoute('node.add_page', [], [
      'query' => ['destination' => '/nodehive/admin/content'],
    ]);
    return new RedirectResponse($url->toString());
  }

  /**
   * Handles quick add content via AJAX.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   JSON response.
   */
  public function quickAdd(Request $request) {
    $content = json_decode($request->getContent(), TRUE);

    $type = $content['type'] ?? '';
    $title = trim($content['title'] ?? '');
    $space = $content['space'] ?? '';
    $author = $content['author'] ?? '';

    // Validate required fields.
    if (empty($type) || empty($title)) {
      return new Response(json_encode([
        'success' => FALSE,
        'message' => $this->t('Content type and title are required.'),
      ]), 400, ['Content-Type' => 'application/json']);
    }

    // Check if content type exists and user has permission to create it.
    $node_type = $this->entityTypeManager->getStorage('node_type')->load($type);
    if (!$node_type) {
      return new Response(json_encode([
        'success' => FALSE,
        'message' => $this->t('Invalid content type.'),
      ]), 400, ['Content-Type' => 'application/json']);
    }

    $access = $this->entityTypeManager->getAccessControlHandler('node')
      ->createAccess($type, $this->currentUser(), [], TRUE);
    if (!$access->isAllowed()) {
      return new Response(json_encode([
        'success' => FALSE,
        'message' => $this->t('You do not have permission to create this content type.'),
      ]), 403, ['Content-Type' => 'application/json']);
    }

    // Create the node.
    $node_data = [
      'type' => $type,
      'title' => $title,
      'status' => 0,
    ];

    // Set space if specified.
    if (!empty($space)) {
      $node_data['nodehive_space'] = (int) $space;
    }

    // Set author if specified.
    if (!empty($author)) {
      $node_data['uid'] = (int) $author;
    }

    try {
      $node = $this->entityTypeManager->getStorage('node')->create($node_data);
      $node->save();

      return new Response(json_encode([
        'success' => TRUE,
        'message' => $this->t('Content "@title" created successfully.', ['@title' => $title]),
        'nid' => $node->id(),
        'edit_url' => $node->toUrl('edit-form', ['query' => ['destination' => '/nodehive/admin/content']])->toString(),
      ]), 200, ['Content-Type' => 'application/json']);
    }
    catch (\Exception $e) {
      return new Response(json_encode([
        'success' => FALSE,
        'message' => $this->t('Failed to create content: @error', ['@error' => $e->getMessage()]),
      ]), 500, ['Content-Type' => 'application/json']);
    }
  }

  /**
   * Handles AJAX filtering via htmx.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   The response.
   */
  public function filter(Request $request) {
    $display_lang = $request->query->get('display_lang', '');
    $space = $request->query->get('space');
    $no_space = ($space === '__none__');
    $space_id = (!$no_space && $space !== NULL && $space !== '') ? (int) $space : NULL;
    $result = $this->getNodes($request, $display_lang, $space_id);
    $nodes = $result['nodes'];
    $pager = $result['pager'];
    $view = $request->query->get('view', 'dense');

    // Build filter conditions for count queries (excluding type).
    $count_conditions = [];

    $title = $request->query->get('title');
    if ($title) {
      $count_conditions['title'] = ['value' => '%' . $title . '%', 'operator' => 'LIKE'];
    }

    $status = $request->query->get('status');
    if ($status !== NULL && $status !== '') {
      $count_conditions['status'] = (int) $status;
    }

    $author = $request->query->get('author');
    if ($author !== NULL && $author !== '') {
      $count_conditions['uid'] = (int) $author;
    }

    if ($no_space) {
      $count_conditions['__no_space'] = TRUE;
    }
    elseif ($space_id !== NULL) {
      $count_conditions['nodehive_space'] = $space_id;
    }

    // Get filtered counts for sidebar.
    $node_type_counts = $this->getNodeTypeCounts($count_conditions);
    $node_types = $this->getNodeTypes();

    // Determine the destination from the originating page URL.
    $hxCurrentUrl = $request->headers->get('HX-Current-URL', '');
    $destination = $hxCurrentUrl ? (parse_url($hxCurrentUrl, PHP_URL_PATH) ?: '/nodehive/admin/content') : '/nodehive/admin/content';

    // Build all three views - client will show/hide based on selection.
    $list_build = [
      '#theme' => 'nodehive_content_table',
      '#nodes' => $nodes,
      '#destination' => $destination,
    ];

    $grid_build = [
      '#theme' => 'nodehive_content_grid',
      '#nodes' => $nodes,
      '#destination' => $destination,
    ];

    // Build node-to-spaces mapping when viewing all spaces.
    $node_spaces = [];
    if ($space_id === NULL && !empty($nodes)) {
      $spaces = $this->getSpaces();
      $node_spaces = $this->getNodeSpaces(array_column($nodes, 'id'), $spaces);
    }

    $dense_build = [
      '#theme' => 'nodehive_content_dense',
      '#nodes' => $nodes,
      '#destination' => $destination,
      '#node_spaces' => $node_spaces,
      '#show_spaces_column' => !empty($node_spaces),
    ];

    $pager_build = [
      '#theme' => 'nodehive_content_pager',
      '#pager' => $pager,
    ];

    $list_html = $this->renderer->renderRoot($list_build);
    $grid_html = $this->renderer->renderRoot($grid_build);
    $dense_html = $this->renderer->renderRoot($dense_build);
    $pager_html = $this->renderer->renderRoot($pager_build);

    $list_hidden = $view !== 'list' ? ' is-hidden' : '';
    $grid_hidden = $view !== 'grid' ? ' is-hidden' : '';
    $dense_hidden = $view !== 'dense' ? ' is-hidden' : '';

    $html = '<div id="content-x-list-wrapper" class="' . $list_hidden . '">' . $list_html . '</div>';
    $html .= '<div id="content-x-grid-wrapper" class="' . $grid_hidden . '">' . $grid_html . '</div>';
    $html .= '<div id="content-x-dense-wrapper" class="' . $dense_hidden . '">' . $dense_html . '</div>';
    $html .= $pager_html;

    // Out-of-band swap to update the results count.
    $total_items = $pager['total_items'];
    $items_label = $this->t('items');
    $html .= '<div id="content-x-results-count" class="content-x-results-count" hx-swap-oob="true"><strong>' . $total_items . '</strong> ' . $items_label . '</div>';

    // Out-of-band swaps to update sidebar counts.
    // Update "All" count.
    $html .= '<span id="content-x-count-all" class="content-x-filter-count" hx-swap-oob="true">' . $node_type_counts['_total'] . '</span>';

    // Update each content type count.
    foreach ($node_types as $type_id => $type_label) {
      $count = $node_type_counts[$type_id] ?? 0;
      $html .= '<span id="content-x-count-' . $type_id . '" class="content-x-filter-count" hx-swap-oob="true">' . $count . '</span>';
    }

    return new Response($html);
  }

  /**
   * Gets filtered nodes with pagination.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param string $display_lang
   *   The language code to display content in.
   * @param int|null $space_id
   *   Optional space ID for building preview URLs.
   *
   * @return array
   *   Array with 'nodes' and 'pager' keys.
   */
  protected function getNodes(Request $request, $display_lang = '', $space_id = NULL) {
    $storage = $this->entityTypeManager->getStorage('node');
    $items_per_page = (int) $request->query->get('limit', 25);
    // Validate items per page to allowed values.
    $allowed_limits = [10, 25, 50, 100];
    if (!in_array($items_per_page, $allowed_limits)) {
      $items_per_page = 25;
    }
    $page = (int) $request->query->get('page', 0);

    // Build base query conditions.
    $conditions = [];

    $title = $request->query->get('title');
    if ($title) {
      $conditions['title'] = ['value' => '%' . $title . '%', 'operator' => 'LIKE'];
    }

    $type = $request->query->get('type');
    if ($type) {
      $conditions['type'] = $type;
    }

    $status = $request->query->get('status');
    if ($status !== NULL && $status !== '') {
      $conditions['status'] = (int) $status;
    }

    $author = $request->query->get('author');
    if ($author !== NULL && $author !== '') {
      $conditions['uid'] = (int) $author;
    }

    $space = $request->query->get('space');
    $no_space = ($space === '__none__');
    if (!$no_space && $space !== NULL && $space !== '') {
      $conditions['nodehive_space'] = (int) $space;
    }

    // Get total count first.
    $count_query = $storage->getQuery()->accessCheck(FALSE);
    foreach ($conditions as $field => $value) {
      if (is_array($value)) {
        $count_query->condition($field, $value['value'], $value['operator']);
      }
      else {
        $count_query->condition($field, $value);
      }
    }
    if ($no_space) {
      $count_query->notExists('nodehive_space');
    }
    $total = (int) $count_query->count()->execute();

    // Calculate pager info.
    $total_pages = $total > 0 ? (int) ceil($total / $items_per_page) : 0;
    $page = max(0, min($page, $total_pages - 1));

    // Build the main query with pagination.
    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->sort('changed', 'DESC')
      ->range($page * $items_per_page, $items_per_page);

    foreach ($conditions as $field => $value) {
      if (is_array($value)) {
        $query->condition($field, $value['value'], $value['operator']);
      }
      else {
        $query->condition($field, $value);
      }
    }
    if ($no_space) {
      $query->notExists('nodehive_space');
    }

    $nids = $query->execute();

    if (empty($nids)) {
      return [
        'nodes' => [],
        'pager' => [
          'current_page' => 0,
          'total_pages' => 0,
          'total_items' => 0,
          'items_per_page' => $items_per_page,
        ],
      ];
    }

    $nodes = $storage->loadMultiple($nids);
    $node_data = [];

    foreach ($nodes as $node) {
      // Determine which node version to use for display.
      $display_node = $node;
      $display_langcode = $node->language()->getId();

      if ($display_lang && $display_lang !== $node->language()->getId()) {
        // Try to get the requested translation.
        if ($node->hasTranslation($display_lang)) {
          $display_node = $node->getTranslation($display_lang);
          $display_langcode = $display_lang;
        }
      }

      $node_data[] = [
        'id' => $node->id(),
        'title' => $display_node->getTitle(),
        'type' => $node->type->entity->label(),
        'type_id' => $node->bundle(),
        'status' => $display_node->isPublished(),
        'author' => $node->getOwner()->getDisplayName(),
        'changed' => $display_node->getChangedTime(),
        'changed_ago' => $this->dateFormatter->formatTimeDiffSince($display_node->getChangedTime(), ['granularity' => 1]),
        'langcode' => $node->language()->getId(),
        'language' => $node->language()->getName(),
        'display_langcode' => $display_langcode,
        'translations' => $this->getNodeTranslations($node),
        'view_url' => $node->toUrl('canonical')->toString(),
        'edit_url' => $display_node->toUrl('edit-form')->toString(),
        'delete_url' => $node->toUrl('delete-form')->toString(),
        'thumbnail' => $this->getNodeThumbnail($display_node),
        'preview_url' => $this->getNodePreviewUrl($display_node, $space_id),
        'frontend_url' => $this->getNodeFrontendUrl($display_node, $space_id),
      ];
    }

    return [
      'nodes' => $node_data,
      'pager' => [
        'current_page' => $page,
        'total_pages' => $total_pages,
        'total_items' => $total,
        'items_per_page' => $items_per_page,
      ],
    ];
  }

  /**
   * Gets a thumbnail URL for a node.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   *
   * @return string|null
   *   The thumbnail URL or NULL if not found.
   */
  protected function getNodeThumbnail($node) {
    // Check for configured field first.
    $config = $this->config('nodehive_content.settings');
    $teaser_image_fields = $config->get('teaser_image_fields') ?? [];
    $bundle = $node->bundle();

    // Build the list of fields to check.
    $image_fields = [];

    // If there's a configured field for this bundle, check it first.
    if (!empty($teaser_image_fields[$bundle])) {
      $image_fields[] = $teaser_image_fields[$bundle];
    }

    // Fall back to common field names for images/media.
    $fallback_fields = [
      'field_media_image',
      'field_image',
      'field_hero_image',
      'field_thumbnail',
      'field_teaser_image',
      'field_featured_image',
      'field_header_image',
      'field_cover_image',
      'field_main_image',
      'field_photo',
      'field_picture',
      'field_media',
      'field_hero_media',
      'field_teaser_media',
    ];

    $image_fields = array_merge($image_fields, $fallback_fields);
    // Remove duplicates.
    $image_fields = array_unique($image_fields);

    foreach ($image_fields as $field_name) {
      if (!$node->hasField($field_name)) {
        continue;
      }

      $field = $node->get($field_name);
      if ($field->isEmpty()) {
        continue;
      }

      $entity = $field->entity;
      if (!$entity) {
        continue;
      }

      // Handle media entities.
      if ($entity->getEntityTypeId() === 'media') {
        $thumbnail = $this->getThumbnailFromMedia($entity);
        if ($thumbnail) {
          return $thumbnail;
        }
      }

      // Handle file entities (direct image fields).
      if ($entity->getEntityTypeId() === 'file') {
        $uri = $entity->getFileUri();
        $style = $this->entityTypeManager->getStorage('image_style')->load('medium');
        if ($style) {
          return $style->buildUrl($uri);
        }
        return $this->fileUrlGenerator->generateAbsoluteString($uri);
      }
    }

    return NULL;
  }

  /**
   * Gets a thumbnail URL from a media entity.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   *
   * @return string|null
   *   The thumbnail URL or NULL if not found.
   */
  protected function getThumbnailFromMedia($media) {
    // Try to get the thumbnail field first.
    if ($media->hasField('thumbnail') && !$media->get('thumbnail')->isEmpty()) {
      $file = $media->get('thumbnail')->entity;
      if ($file) {
        $uri = $file->getFileUri();
        $style = $this->entityTypeManager->getStorage('image_style')->load('medium');
        if ($style) {
          return $style->buildUrl($uri);
        }
        return $this->fileUrlGenerator->generateAbsoluteString($uri);
      }
    }

    // Try common source fields.
    $source_fields = [
      'field_media_image',
      'field_media_video_file',
      'field_media_document',
      'field_media_file',
    ];

    foreach ($source_fields as $field_name) {
      if (!$media->hasField($field_name)) {
        continue;
      }

      $field = $media->get($field_name);
      if ($field->isEmpty()) {
        continue;
      }

      $file = $field->entity;
      if (!$file) {
        continue;
      }

      $uri = $file->getFileUri();
      $mime = $file->getMimeType();

      // Only generate styled URLs for images.
      if (strpos($mime, 'image/') === 0) {
        $style = $this->entityTypeManager->getStorage('image_style')->load('medium');
        if ($style) {
          return $style->buildUrl($uri);
        }
        return $this->fileUrlGenerator->generateAbsoluteString($uri);
      }
    }

    return NULL;
  }

  /**
   * Gets translation information for a node.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   *
   * @return array
   *   Array of translation data.
   */
  protected function getNodeTranslations($node) {
    $translations = [];

    // Check if content translation is enabled for this bundle.
    $bundle = $node->bundle();
    $content_translation_manager = $this->contentTranslationManager;
    $is_translatable = $content_translation_manager->isEnabled('node', $bundle);

    if (!$is_translatable) {
      // Not translatable, just return the current language.
      $translations[] = [
        'langcode' => $node->language()->getId(),
        'language' => $node->language()->getName(),
        'status' => $node->isPublished(),
        'exists' => TRUE,
        'edit_url' => $node->toUrl('edit-form')->toString(),
      ];
      return $translations;
    }

    // Get all enabled languages.
    $language_manager = $this->languageManager();
    $all_languages = $language_manager->getLanguages();
    $existing_translations = $node->getTranslationLanguages();

    foreach ($all_languages as $langcode => $language) {
      if (isset($existing_translations[$langcode])) {
        // Translation exists.
        $translation = $node->getTranslation($langcode);
        $translations[] = [
          'langcode' => $langcode,
          'language' => $language->getName(),
          'status' => $translation->isPublished(),
          'exists' => TRUE,
          'edit_url' => $translation->toUrl('edit-form')->toString(),
        ];
      }
      else {
        // Translation does not exist.
        $translations[] = [
          'langcode' => $langcode,
          'language' => $language->getName(),
          'status' => FALSE,
          'exists' => FALSE,
          'add_url' => '/node/' . $node->id() . '/translations/add/' . $node->language()->getId() . '/' . $langcode,
        ];
      }
    }

    return $translations;
  }

  /**
   * Gets a preview URL for a node in the context of a space.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   * @param int|null $space_id
   *   The space ID, or NULL if not in space context.
   *
   * @return string|null
   *   The preview URL or NULL if not applicable.
   */
  protected function getNodePreviewUrl($node, $space_id = NULL) {
    if (!$space_id) {
      return NULL;
    }

    // Check if the nodehive_space entity type exists.
    if (!$this->entityTypeManager->hasDefinition('nodehive_space')) {
      return NULL;
    }

    $space = $this->entityTypeManager->getStorage('nodehive_space')->load($space_id);
    if (!$space || !$space->hasField('space_url')) {
      return NULL;
    }

    $space_urls = $space->get('space_url')->getValue();
    if (empty($space_urls)) {
      return NULL;
    }

    $base_url = $space_urls[0]['uri'] ?? NULL;
    if (!$base_url) {
      return NULL;
    }

    // Build the visual editor URL with the frontend URL as query parameter.
    $internal_path = $node->toUrl()->toString();
    $frontend_url = rtrim($base_url, '/') . $internal_path;

    return Url::fromRoute('nodehive_visualeditor.visual_editor_single', [
      'nodehive_space' => $space_id,
    ], [
      'query' => ['url' => $frontend_url],
    ])->toString();
  }

  /**
   * Gets the external frontend URL for a node within a space.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   * @param int|null $space_id
   *   The space ID, or NULL if not in space context.
   *
   * @return string|null
   *   The frontend URL or NULL if not applicable.
   */
  protected function getNodeFrontendUrl($node, $space_id = NULL) {
    if (!$space_id) {
      return NULL;
    }

    if (!$this->entityTypeManager->hasDefinition('nodehive_space')) {
      return NULL;
    }

    $space = $this->entityTypeManager->getStorage('nodehive_space')->load($space_id);
    if (!$space || !$space->hasField('space_url')) {
      return NULL;
    }

    $space_urls = $space->get('space_url')->getValue();
    if (empty($space_urls)) {
      return NULL;
    }

    $base_url = $space_urls[0]['uri'] ?? NULL;
    if (!$base_url) {
      return NULL;
    }

    return rtrim($base_url, '/') . $node->toUrl()->toString();
  }

  /**
   * Gets the spaces each node belongs to.
   *
   * @param array $node_ids
   *   Array of node IDs.
   * @param array $spaces
   *   Array of space labels keyed by space ID.
   *
   * @return array
   *   Array keyed by node ID, each containing an array of space labels.
   */
  protected function getNodeSpaces(array $node_ids, array $spaces) {
    if (empty($node_ids) || empty($spaces)) {
      return [];
    }

    if (!$this->entityTypeManager->hasDefinition('nodehive_space')) {
      return [];
    }

    // Query nodes that belong to any of the given spaces and are in our list.
    $matched_ids = $this->entityTypeManager->getStorage('node')->getQuery()
      ->accessCheck(FALSE)
      ->condition('nid', $node_ids, 'IN')
      ->condition('nodehive_space', array_keys($spaces), 'IN')
      ->execute();

    $node_spaces = [];
    foreach ($this->entityTypeManager->getStorage('node')->loadMultiple($matched_ids) as $node) {
      foreach ($node->get('nodehive_space')->referencedEntities() as $space) {
        if (!isset($spaces[$space->id()])) {
          continue;
        }
        $node_spaces[$node->id()][] = [
          'label' => $space->label(),
          'url' => $space->toUrl()->toString(),
        ];
      }
    }

    return $node_spaces;
  }

  /**
   * Gets all node types.
   *
   * @return array
   *   Array of node type labels keyed by machine name.
   */
  protected function getNodeTypes() {
    $node_types = $this->entityTypeManager
      ->getStorage('node_type')
      ->loadMultiple();

    $types = [];
    foreach ($node_types as $type) {
      $types[$type->id()] = $type->label();
    }

    return $types;
  }

  /**
   * Gets node counts per content type.
   *
   * @param array $conditions
   *   Optional filter conditions to apply (excluding type).
   *
   * @return array
   *   Array of node counts keyed by content type machine name.
   */
  protected function getNodeTypeCounts(array $conditions = []) {
    $storage = $this->entityTypeManager->getStorage('node');
    $node_types = $this->entityTypeManager
      ->getStorage('node_type')
      ->loadMultiple();

    $counts = [];
    $total = 0;

    foreach ($node_types as $type) {
      $type_id = $type->id();
      $query = $storage->getQuery()
        ->accessCheck(FALSE)
        ->condition('type', $type_id);

      // Apply additional filter conditions.
      foreach ($conditions as $field => $value) {
        if ($field === '__no_space') {
          $query->notExists('nodehive_space');
          continue;
        }
        if (is_array($value)) {
          $query->condition($field, $value['value'], $value['operator']);
        }
        else {
          $query->condition($field, $value);
        }
      }

      $count = (int) $query->count()->execute();
      $counts[$type_id] = $count;
      $total += $count;
    }

    // Add total count for "All" option.
    $counts['_total'] = $total;

    return $counts;
  }

  /**
   * Gets all enabled languages.
   *
   * @return array
   *   Array of language names keyed by langcode.
   */
  protected function getEnabledLanguages() {
    $language_manager = $this->languageManager();
    $languages = $language_manager->getLanguages();

    $result = [];
    foreach ($languages as $langcode => $language) {
      $result[$langcode] = $language->getName();
    }

    return $result;
  }

  /**
   * Gets all users who have authored content.
   *
   * @return array
   *   Array of user display names keyed by user ID.
   */
  protected function getContentAuthors() {
    $storage = $this->entityTypeManager->getStorage('node');

    // Get distinct author UIDs from nodes.
    $query = $storage->getQuery()
      ->accessCheck(FALSE);
    $nids = $query->execute();

    if (empty($nids)) {
      return [];
    }

    // Load nodes and collect unique author UIDs.
    $nodes = $storage->loadMultiple($nids);
    $author_uids = [];
    foreach ($nodes as $node) {
      $uid = $node->getOwnerId();
      if ($uid && !isset($author_uids[$uid])) {
        $author_uids[$uid] = $uid;
      }
    }

    if (empty($author_uids)) {
      return [];
    }

    // Load users and build the result array.
    $user_storage = $this->entityTypeManager->getStorage('user');
    $users = $user_storage->loadMultiple($author_uids);

    $authors = [];
    foreach ($users as $user) {
      $authors[$user->id()] = $user->getDisplayName();
    }

    // Sort by display name.
    asort($authors);

    return $authors;
  }

  /**
   * Gets NodeHive spaces accessible by the current user.
   *
   * @return array
   *   Array of space labels keyed by space ID.
   */
  protected function getSpaces() {
    // Check if the nodehive_space entity type exists.
    if (!$this->entityTypeManager->hasDefinition('nodehive_space')) {
      return [];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_space');
    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->sort('label', 'ASC');

    // Filter by user's space access unless admin.
    $current_user = $this->currentUser();
    if (!in_array('administrator', $current_user->getRoles()) && (int) $current_user->id() !== 1) {
      $query->condition('editors', $current_user->id());
    }

    $space_ids = $query->execute();

    if (empty($space_ids)) {
      return [];
    }

    $spaces = $storage->loadMultiple($space_ids);
    $result = [];

    foreach ($spaces as $space) {
      $result[$space->id()] = $space->label();
    }

    return $result;
  }

  /**
   * Returns spaces grouped by status for filter dropdowns.
   */
  protected function getSpaceGroups(array $spaces): array {
    if (empty($spaces)) {
      return [];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_space');
    $entities = $storage->loadMultiple(array_keys($spaces));
    $groups = ['active' => [], 'disabled' => []];

    foreach ($entities as $space) {
      $key = !$space->get('status')->isEmpty() && $space->get('status')->value ? 'active' : 'disabled';
      $groups[$key][$space->id()] = $space->label();
    }

    return $groups;
  }

  /**
   * Displays the fragments overview page.
   */
  public function fragments(Request $request) {
    $display_lang = $request->query->get('display_lang', '');

    $current_user = $this->currentUser();
    $user_roles = $current_user->getRoles();
    $show_all_spaces = in_array('administrator', $user_roles)
      || in_array('nodehive_admin', $user_roles)
      || in_array('nodehive_owner', $user_roles)
      || (int) $current_user->id() === 1;

    $spaces = $this->getSpaces();

    $space = $request->query->get('space');
    if ($space === NULL || $space === '') {
      $cookie_space = $request->cookies->get('nodehive_content_fragments_space')
        ?? $request->cookies->get('nodehive_content_space');
      if ($cookie_space !== NULL && $cookie_space !== '') {
        $request->query->set('space', $cookie_space);
      }
    }

    $active_space = $request->query->get('space', '');
    $no_space = ($active_space === '__none__');
    $space_id = (!$no_space && $active_space !== NULL && $active_space !== '') ? (int) $active_space : NULL;

    if (!$show_all_spaces && !empty($spaces)) {
      if (!$no_space && ($space_id === NULL || !isset($spaces[$space_id]))) {
        $space_id = (int) array_key_first($spaces);
        $request->query->set('space', (string) $space_id);
      }
    }

    $result = $this->getFragments($request, $display_lang);
    $fragment_types = $this->getFragmentTypes();

    $count_conditions = [];
    if ($no_space) {
      $count_conditions['__no_space'] = TRUE;
    }
    elseif ($space_id !== NULL) {
      $count_conditions['space_id'] = $space_id;
    }
    $fragment_type_counts = $this->getFragmentTypeCounts($count_conditions);

    $languages = $this->getEnabledLanguages();
    $authors = $this->getFragmentAuthors();

    $fragment_spaces = [];
    if ($space_id === NULL && !empty($result['fragments'])) {
      $fragment_spaces = $this->getFragmentSpaces(array_column($result['fragments'], 'id'), $spaces);
    }

    return [
      '#theme' => 'nodehive_content_fragments_overview',
      '#fragments' => $result['fragments'],
      '#pager' => $result['pager'],
      '#fragment_types' => $fragment_types,
      '#fragment_type_counts' => $fragment_type_counts,
      '#languages' => $languages,
      '#authors' => $authors,
      '#spaces' => $spaces,
      '#space_groups' => $this->getSpaceGroups($spaces),
      '#fragment_spaces' => $fragment_spaces,
      '#show_all_spaces' => $show_all_spaces,
      '#hide_space_filter' => (bool) $request->attributes->get('hide_space_filter', FALSE),
      '#filter_url' => '/nodehive/admin/content/fragments/filter',
      '#base_url' => '/nodehive/admin/content/fragments',
      '#destination' => $request->getPathInfo(),
      '#filters' => [
        'title' => $request->query->get('title', ''),
        'type' => $request->query->get('type', ''),
        'status' => $request->query->get('status', ''),
        'author' => $request->query->get('author', ''),
        'space' => $request->query->get('space', ''),
        'display_lang' => $display_lang,
        'page' => $result['pager']['current_page'],
        'limit' => $result['pager']['items_per_page'],
      ],
      '#attached' => [
        'library' => [
          'nodehive_content/nodehive_content',
        ],
      ],
    ];
  }

  /**
   * Handles AJAX filtering for fragments via htmx.
   */
  public function fragmentsFilter(Request $request) {
    $display_lang = $request->query->get('display_lang', '');
    $space = $request->query->get('space');
    $no_space = ($space === '__none__');
    $space_id = (!$no_space && $space !== NULL && $space !== '') ? (int) $space : NULL;
    $result = $this->getFragments($request, $display_lang);
    $fragments = $result['fragments'];
    $pager = $result['pager'];
    $view = $request->query->get('view', 'dense');

    $count_conditions = [];

    $title = $request->query->get('title');
    if ($title) {
      $count_conditions['title'] = ['value' => '%' . $title . '%', 'operator' => 'LIKE'];
    }

    $status = $request->query->get('status');
    if ($status !== NULL && $status !== '') {
      $count_conditions['status'] = (int) $status;
    }

    $author = $request->query->get('author');
    if ($author !== NULL && $author !== '') {
      $count_conditions['uid'] = (int) $author;
    }

    if ($no_space) {
      $count_conditions['__no_space'] = TRUE;
    }
    elseif ($space_id !== NULL) {
      $count_conditions['space_id'] = $space_id;
    }

    $fragment_type_counts = $this->getFragmentTypeCounts($count_conditions);
    $fragment_types = $this->getFragmentTypes();

    $hxCurrentUrl = $request->headers->get('HX-Current-URL', '');
    $destination = $hxCurrentUrl ? (parse_url($hxCurrentUrl, PHP_URL_PATH) ?: '/nodehive/admin/content/fragments') : '/nodehive/admin/content/fragments';

    $list_build = [
      '#theme' => 'nodehive_content_fragments_table',
      '#fragments' => $fragments,
      '#destination' => $destination,
    ];

    $grid_build = [
      '#theme' => 'nodehive_content_fragments_grid',
      '#fragments' => $fragments,
      '#destination' => $destination,
    ];

    $fragment_spaces = [];
    if ($space_id === NULL && !empty($fragments)) {
      $spaces = $this->getSpaces();
      $fragment_spaces = $this->getFragmentSpaces(array_column($fragments, 'id'), $spaces);
    }

    $dense_build = [
      '#theme' => 'nodehive_content_fragments_dense',
      '#fragments' => $fragments,
      '#destination' => $destination,
      '#fragment_spaces' => $fragment_spaces,
      '#show_spaces_column' => !empty($fragment_spaces),
    ];

    $filter_url = $request->getPathInfo();
    $pager_build = [
      '#theme' => 'nodehive_content_fragments_pager',
      '#pager' => $pager,
      '#filter_url' => $filter_url,
    ];

    $list_html = $this->renderer->renderRoot($list_build);
    $grid_html = $this->renderer->renderRoot($grid_build);
    $dense_html = $this->renderer->renderRoot($dense_build);
    $pager_html = $this->renderer->renderRoot($pager_build);

    $list_hidden = $view !== 'list' ? ' is-hidden' : '';
    $grid_hidden = $view !== 'grid' ? ' is-hidden' : '';
    $dense_hidden = $view !== 'dense' ? ' is-hidden' : '';

    $html = '<div id="content-x-list-wrapper" class="' . $list_hidden . '">' . $list_html . '</div>';
    $html .= '<div id="content-x-grid-wrapper" class="' . $grid_hidden . '">' . $grid_html . '</div>';
    $html .= '<div id="content-x-dense-wrapper" class="' . $dense_hidden . '">' . $dense_html . '</div>';
    $html .= $pager_html;

    $total_items = $pager['total_items'];
    $items_label = $this->t('items');
    $html .= '<div id="content-x-results-count" class="content-x-results-count" hx-swap-oob="true"><strong>' . $total_items . '</strong> ' . $items_label . '</div>';

    $html .= '<span id="content-x-count-all" class="content-x-filter-count" hx-swap-oob="true">' . $fragment_type_counts['_total'] . '</span>';

    foreach ($fragment_types as $type_id => $type_label) {
      $count = $fragment_type_counts[$type_id] ?? 0;
      $html .= '<span id="content-x-count-' . $type_id . '" class="content-x-filter-count" hx-swap-oob="true">' . $count . '</span>';
    }

    return new Response($html);
  }

  /**
   * Displays fragments overview within a space context.
   */
  public function spaceFragments($nodehive_space, Request $request) {
    $space_id = is_object($nodehive_space) ? $nodehive_space->id() : $nodehive_space;
    $request->query->set('space', (string) $space_id);
    $request->attributes->set('hide_space_filter', TRUE);
    $build = $this->fragments($request);
    $build['#filter_url'] = '/space/' . $space_id . '/content/fragments/filter';
    $build['#base_url'] = '/space/' . $space_id . '/content/fragments';
    $build['#filters']['space'] = (string) $space_id;
    return $build;
  }

  /**
   * Handles AJAX filtering for fragments within a space context.
   */
  public function spaceFragmentsFilter($nodehive_space, Request $request) {
    $space_id = is_object($nodehive_space) ? $nodehive_space->id() : $nodehive_space;
    $request->query->set('space', (string) $space_id);
    return $this->fragmentsFilter($request);
  }

  /**
   * Gets filtered fragments with pagination.
   */
  protected function getFragments(Request $request, $display_lang = '') {
    if (!$this->entityTypeManager->hasDefinition('nodehive_fragment')) {
      return [
        'fragments' => [],
        'pager' => [
          'current_page' => 0,
          'total_pages' => 0,
          'total_items' => 0,
          'items_per_page' => 25,
        ],
      ];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_fragment');
    $items_per_page = (int) $request->query->get('limit', 25);
    $allowed_limits = [10, 25, 50, 100];
    if (!in_array($items_per_page, $allowed_limits)) {
      $items_per_page = 25;
    }
    $page = (int) $request->query->get('page', 0);

    $conditions = [];

    $title = $request->query->get('title');
    if ($title) {
      $conditions['title'] = ['value' => '%' . $title . '%', 'operator' => 'LIKE'];
    }

    $type = $request->query->get('type');
    if ($type) {
      $conditions['type'] = $type;
    }

    $status = $request->query->get('status');
    if ($status !== NULL && $status !== '') {
      $conditions['status'] = (int) $status;
    }

    $author = $request->query->get('author');
    if ($author !== NULL && $author !== '') {
      $conditions['uid'] = (int) $author;
    }

    $space = $request->query->get('space');
    $no_space = ($space === '__none__');
    if (!$no_space && $space !== NULL && $space !== '') {
      $conditions['space_id'] = (int) $space;
    }

    $count_query = $storage->getQuery()->accessCheck(FALSE);
    foreach ($conditions as $field => $value) {
      if (is_array($value)) {
        $count_query->condition($field, $value['value'], $value['operator']);
      }
      else {
        $count_query->condition($field, $value);
      }
    }
    if ($no_space) {
      $count_query->notExists('space_id');
    }
    $total = (int) $count_query->count()->execute();

    $total_pages = $total > 0 ? (int) ceil($total / $items_per_page) : 0;
    $page = max(0, min($page, $total_pages - 1));

    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->sort('changed', 'DESC')
      ->range($page * $items_per_page, $items_per_page);

    foreach ($conditions as $field => $value) {
      if (is_array($value)) {
        $query->condition($field, $value['value'], $value['operator']);
      }
      else {
        $query->condition($field, $value);
      }
    }
    if ($no_space) {
      $query->notExists('space_id');
    }

    $fids = $query->execute();

    if (empty($fids)) {
      return [
        'fragments' => [],
        'pager' => [
          'current_page' => 0,
          'total_pages' => 0,
          'total_items' => 0,
          'items_per_page' => $items_per_page,
        ],
      ];
    }

    $fragments = $storage->loadMultiple($fids);
    $fragment_data = [];

    foreach ($fragments as $fragment) {
      $display_fragment = $fragment;
      $display_langcode = $fragment->language()->getId();

      if ($display_lang && $display_lang !== $fragment->language()->getId()) {
        if ($fragment->hasTranslation($display_lang)) {
          $display_fragment = $fragment->getTranslation($display_lang);
          $display_langcode = $display_lang;
        }
      }

      $fragment_data[] = [
        'id' => $fragment->id(),
        'title' => $display_fragment->getTitle(),
        'type' => $fragment->type->entity ? $fragment->type->entity->label() : $fragment->bundle(),
        'type_id' => $fragment->bundle(),
        'status' => $display_fragment->isPublished(),
        'author' => $fragment->getOwner() ? $fragment->getOwner()->getDisplayName() : '',
        'changed' => $display_fragment->getChangedTime(),
        'changed_ago' => $this->dateFormatter->formatTimeDiffSince($display_fragment->getChangedTime(), ['granularity' => 1]),
        'langcode' => $fragment->language()->getId(),
        'language' => $fragment->language()->getName(),
        'display_langcode' => $display_langcode,
        'translations' => $this->getFragmentTranslations($fragment),
        'edit_url' => $fragment->toUrl('edit-form')->toString(),
      ];
    }

    return [
      'fragments' => $fragment_data,
      'pager' => [
        'current_page' => $page,
        'total_pages' => $total_pages,
        'total_items' => $total,
        'items_per_page' => $items_per_page,
      ],
    ];
  }

  /**
   * Gets translation information for a fragment.
   */
  protected function getFragmentTranslations($fragment) {
    $translations = [];

    $bundle = $fragment->bundle();
    $is_translatable = $this->contentTranslationManager->isEnabled('nodehive_fragment', $bundle);

    if (!$is_translatable) {
      $translations[] = [
        'langcode' => $fragment->language()->getId(),
        'language' => $fragment->language()->getName(),
        'status' => $fragment->isPublished(),
        'exists' => TRUE,
        'edit_url' => $fragment->toUrl('edit-form')->toString(),
      ];
      return $translations;
    }

    $language_manager = $this->languageManager();
    $all_languages = $language_manager->getLanguages();
    $existing_translations = $fragment->getTranslationLanguages();

    foreach ($all_languages as $langcode => $language) {
      if (isset($existing_translations[$langcode])) {
        $translation = $fragment->getTranslation($langcode);
        $translations[] = [
          'langcode' => $langcode,
          'language' => $language->getName(),
          'status' => $translation->isPublished(),
          'exists' => TRUE,
          'edit_url' => $translation->toUrl('edit-form')->toString(),
        ];
      }
      else {
        $translations[] = [
          'langcode' => $langcode,
          'language' => $language->getName(),
          'status' => FALSE,
          'exists' => FALSE,
          'add_url' => '/fragment/' . $fragment->id() . '/translations/add/' . $fragment->language()->getId() . '/' . $langcode,
        ];
      }
    }

    return $translations;
  }

  /**
   * Gets all fragment types.
   */
  protected function getFragmentTypes() {
    if (!$this->entityTypeManager->hasDefinition('nodehive_fragment_type')) {
      return [];
    }

    $fragment_types = $this->entityTypeManager
      ->getStorage('nodehive_fragment_type')
      ->loadMultiple();

    $types = [];
    foreach ($fragment_types as $type) {
      $types[$type->id()] = $type->label();
    }

    return $types;
  }

  /**
   * Gets fragment counts per type.
   */
  protected function getFragmentTypeCounts(array $conditions = []) {
    if (!$this->entityTypeManager->hasDefinition('nodehive_fragment')) {
      return ['_total' => 0];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_fragment');
    $fragment_types = $this->getFragmentTypes();

    $counts = [];
    $total = 0;

    foreach ($fragment_types as $type_id => $type_label) {
      $query = $storage->getQuery()
        ->accessCheck(FALSE)
        ->condition('type', $type_id);

      foreach ($conditions as $field => $value) {
        if ($field === '__no_space') {
          $query->notExists('space_id');
          continue;
        }
        if (is_array($value)) {
          $query->condition($field, $value['value'], $value['operator']);
        }
        else {
          $query->condition($field, $value);
        }
      }

      $count = (int) $query->count()->execute();
      $counts[$type_id] = $count;
      $total += $count;
    }

    $counts['_total'] = $total;

    return $counts;
  }

  /**
   * Gets all users who have authored fragments.
   */
  protected function getFragmentAuthors() {
    if (!$this->entityTypeManager->hasDefinition('nodehive_fragment')) {
      return [];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_fragment');
    $fids = $storage->getQuery()->accessCheck(FALSE)->execute();

    if (empty($fids)) {
      return [];
    }

    $fragments = $storage->loadMultiple($fids);
    $author_uids = [];
    foreach ($fragments as $fragment) {
      $uid = $fragment->getOwnerId();
      if ($uid && !isset($author_uids[$uid])) {
        $author_uids[$uid] = $uid;
      }
    }

    if (empty($author_uids)) {
      return [];
    }

    $user_storage = $this->entityTypeManager->getStorage('user');
    $users = $user_storage->loadMultiple($author_uids);

    $authors = [];
    foreach ($users as $user) {
      $authors[$user->id()] = $user->getDisplayName();
    }

    asort($authors);

    return $authors;
  }

  /**
   * Gets the spaces each fragment belongs to.
   */
  protected function getFragmentSpaces(array $fragment_ids, array $spaces) {
    if (empty($fragment_ids) || empty($spaces)) {
      return [];
    }

    if (!$this->entityTypeManager->hasDefinition('nodehive_fragment')) {
      return [];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_fragment');
    $matched_ids = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('fid', $fragment_ids, 'IN')
      ->condition('space_id', array_keys($spaces), 'IN')
      ->execute();

    $fragment_spaces = [];
    foreach ($storage->loadMultiple($matched_ids) as $fragment) {
      foreach ($fragment->get('space_id')->referencedEntities() as $space) {
        if (!isset($spaces[$space->id()])) {
          continue;
        }
        $fragment_spaces[$fragment->id()][] = [
          'label' => $space->label(),
          'url' => $space->toUrl()->toString(),
        ];
      }
    }

    return $fragment_spaces;
  }

  /**
   * Gets allowed content types for each space accessible by the current user.
   *
   * @return array
   *   Array mapping space IDs to arrays of allowed content type IDs.
   *   Empty array for a space means all types are allowed.
   */
  protected function getSpaceContentTypes() {
    // Check if the nodehive_space entity type exists.
    if (!$this->entityTypeManager->hasDefinition('nodehive_space')) {
      return [];
    }

    $storage = $this->entityTypeManager->getStorage('nodehive_space');
    $query = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('status', 1);

    // Filter by user's space access unless admin.
    $current_user = $this->currentUser();
    if (!in_array('administrator', $current_user->getRoles()) && (int) $current_user->id() !== 1) {
      $query->condition('editors', $current_user->id());
    }

    $space_ids = $query->execute();

    if (empty($space_ids)) {
      return [];
    }

    $spaces = $storage->loadMultiple($space_ids);
    $result = [];

    foreach ($spaces as $space) {
      $space_id = $space->id();
      $allowed_types = [];

      // Get referenced content types from the space.
      if ($space->hasField('content_types') && !$space->get('content_types')->isEmpty()) {
        $content_types = $space->get('content_types')->referencedEntities();
        foreach ($content_types as $type) {
          $allowed_types[] = $type->id();
        }
      }

      // Store the allowed types (empty array means all types allowed).
      $result[$space_id] = $allowed_types;
    }

    return $result;
  }

}
