<?php

declare(strict_types=1);

namespace Drupal\acquia_dam\Hook;

use Drupal\acquia_dam\Entity\MediaItemField;
use Drupal\canvas\PropExpressions\StructuredData\FieldPropExpression;
use Drupal\canvas\PropExpressions\StructuredData\FieldTypeObjectPropsExpression;
use Drupal\canvas\PropExpressions\StructuredData\FieldTypePropExpression;
use Drupal\canvas\PropExpressions\StructuredData\ReferenceFieldTypePropExpression;
use Drupal\canvas\PropShape\CandidateStorablePropShape;
use Drupal\canvas\TypedData\BetterEntityDataDefinition;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;

/**
 * @file
 * Hook implementations for acquia_dam & canvas integration.
 */
class CanvasHooks {

  /**
   * Construct a new CanvasHooks object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(private readonly EntityTypeManagerInterface $entity_type_manager) {}

  /**
   * Mapping of JSON schema $ref values to media source plugin IDs.
   *
   * This is used to determine which media source plugins to add to the list of
   * allowed media types for a given storable prop shape.
   *
   * @see self::acquiaDamStoragePropShapeAlter()
   */
  const SCHEMA_TO_MEDIA_SOURCE_PLUGIN = [
    // @see \Drupal\media\Plugin\media\Source\Image
    'json-schema-definitions://canvas.module/image' => 'acquia_dam_asset:image',
  ];

  /**
   * Implements hook_storage_prop_shape_alter().
   *
   * Overrides (extends, really) Canvas' default choice of using the `image` media
   * type, to also allow \Drupal\acquia_dam\Plugin\media\acquia_dam\Image.
   *
   * @see \Drupal\canvas\Hook\ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter()
   * @see \Drupal\media\Plugin\media\Source\Image
   * @see \Drupal\acquia_dam\Plugin\media\acquia_dam\Image
   */
  #[Hook('storage_prop_shape_alter')]
  public function acquiaDamStoragePropShapeAlter(CandidateStorablePropShape $storable_prop_shape): void {
    if (
      $storable_prop_shape->shape->schema['type'] === 'object'
      && isset($storable_prop_shape->shape->schema['$ref'])
      && array_key_exists($storable_prop_shape->shape->schema['$ref'], self::SCHEMA_TO_MEDIA_SOURCE_PLUGIN)
    ) {
      if ($storable_prop_shape->fieldWidget !== 'media_library_widget') {
        throw new \LogicException('This MUST run after \Drupal\canvas\Hook\ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter(), because it builds on top of what did logic did.');
      }

      // Verifying there's an array of target bundles and each has an array of
      // media source field names being referenced.
      $media_type_ids = &$storable_prop_shape->fieldInstanceSettings['handler_settings']['target_bundles'];
      $field_names_per_media_type_id = $storable_prop_shape->fieldTypeProp->objectPropsToFieldTypeProps['src']->referenced->fieldName;

      if (is_string($field_names_per_media_type_id)) {
        assert(count($media_type_ids) === 1);
        $field_names_per_media_type_id = [
          reset($media_type_ids) => $field_names_per_media_type_id,
        ];
      }

      // TRICKY: all props in the FieldObjectPropsExpression are *guaranteed* to
      // be inside the same field instance.
      // @see \Drupal\experience_builder\PropExpressions\StructuredData\FieldObjectPropsExpression::__construct()
      $src_prop_names_per_field_name = $storable_prop_shape->fieldTypeProp->objectPropsToFieldTypeProps['src']->referenced->propName;
      $alt_prop_names_per_field_name = $storable_prop_shape->fieldTypeProp->objectPropsToFieldTypeProps['alt']->referenced->propName;
      $width_prop_names_per_field_name = $storable_prop_shape->fieldTypeProp->objectPropsToFieldTypeProps['width']->referenced->propName;
      $height_prop_names_per_field_name = $storable_prop_shape->fieldTypeProp->objectPropsToFieldTypeProps['height']->referenced->propName;

      if (is_string($src_prop_names_per_field_name)) {
        assert(count($media_type_ids) >= 1);
        $src_prop_names_per_field_name = $this->upcastPropNameToArray($src_prop_names_per_field_name, $media_type_ids, $field_names_per_media_type_id);
        $alt_prop_names_per_field_name = $this->upcastPropNameToArray($alt_prop_names_per_field_name, $media_type_ids, $field_names_per_media_type_id);
        $width_prop_names_per_field_name = $this->upcastPropNameToArray($width_prop_names_per_field_name, $media_type_ids, $field_names_per_media_type_id);
        $height_prop_names_per_field_name = $this->upcastPropNameToArray($height_prop_names_per_field_name, $media_type_ids, $field_names_per_media_type_id);
      }

      assert(is_array($field_names_per_media_type_id));
      assert(array_keys($field_names_per_media_type_id) === array_keys($media_type_ids));
      assert(array_keys($field_names_per_media_type_id) === array_values($media_type_ids));

      $media_source_plugin = self::SCHEMA_TO_MEDIA_SOURCE_PLUGIN[$storable_prop_shape->shape->schema['$ref']];
      $acquia_dam_media_types = $this->getMediaTypesForSourcePlugins($media_source_plugin);

      // Add each Acquia DAM media type to the allowed list and map its source
      // field name to the corresponding image prop (src, alt, width, height).
      foreach ($acquia_dam_media_types as $bundle => $media_type) {
        $source_plugin_id = $media_type->getSource()->getPluginDefinition()['id'] ?? '';
        assert(is_string($source_plugin_id) && !empty($source_plugin_id), 'Media source plugin ID must be a non-empty string.');
        $fieldName = MediaItemField::getFieldName($source_plugin_id);
        $media_type_ids[$bundle] = $bundle;
        $field_names_per_media_type_id[$bundle] = $fieldName;
        $src_prop_names_per_field_name[$fieldName] = 'src_with_alternate_widths';
        $alt_prop_names_per_field_name[$fieldName] = 'alt';
        $width_prop_names_per_field_name[$fieldName] = 'width';
        $height_prop_names_per_field_name[$fieldName] = 'height';
      }

      // Target bundles must be sorted.
      // @see \Drupal\canvas\TypedData\BetterEntityDataDefinition::create()
      ksort($media_type_ids);
      // Field mapping must match the sort order.
      ksort($field_names_per_media_type_id);

      // Property names must match the sort order of field names.
      // This ensures that when multiple media types are targeted, the property
      // names are aligned with their respective field names.
      // @see \Drupal\canvas\PropExpressions\StructuredData\FieldPropExpression::__construct()
      $src_prop_names_per_field_name = $this->sortPropNamesByFieldOrder($src_prop_names_per_field_name, $field_names_per_media_type_id);
      $alt_prop_names_per_field_name = $this->sortPropNamesByFieldOrder($alt_prop_names_per_field_name, $field_names_per_media_type_id);
      $width_prop_names_per_field_name = $this->sortPropNamesByFieldOrder($width_prop_names_per_field_name, $field_names_per_media_type_id);
      $height_prop_names_per_field_name = $this->sortPropNamesByFieldOrder($height_prop_names_per_field_name, $field_names_per_media_type_id);

      // Now, update the expression with property mappings created above.
      // Each property ('src', 'alt', 'width', 'height') is mapped to its field
      // and prop names, ensuring alignment with the sorted field names and
      // media bundles.
      // @see \Drupal\canvas\Hook\ShapeMatchingHooks::mediaLibraryStoragePropShapeAlter()
      $storable_prop_shape->fieldTypeProp = new FieldTypeObjectPropsExpression('entity_reference', [
        'src' => new ReferenceFieldTypePropExpression(
          new FieldTypePropExpression('entity_reference', 'entity'),
          new FieldPropExpression(BetterEntityDataDefinition::create('media', array_values($media_type_ids)), $field_names_per_media_type_id, \NULL, $src_prop_names_per_field_name),
        ),
        'alt' => new ReferenceFieldTypePropExpression(new FieldTypePropExpression('entity_reference', 'entity'), new FieldPropExpression(BetterEntityDataDefinition::create('media', array_values($media_type_ids)), $field_names_per_media_type_id, \NULL, $alt_prop_names_per_field_name)),
        'width' => new ReferenceFieldTypePropExpression(new FieldTypePropExpression('entity_reference', 'entity'), new FieldPropExpression(BetterEntityDataDefinition::create('media', array_values($media_type_ids)), $field_names_per_media_type_id, \NULL, $width_prop_names_per_field_name)),
        'height' => new ReferenceFieldTypePropExpression(new FieldTypePropExpression('entity_reference', 'entity'), new FieldPropExpression(BetterEntityDataDefinition::create('media', array_values($media_type_ids)), $field_names_per_media_type_id, \NULL, $height_prop_names_per_field_name)),
      ]);
    }
  }

  /**
   * Get all MediaTypes that use the given source plugin.
   *
   * @param string $media_source_plugin
   *   The media source plugin ID.
   *
   * @return \Drupal\media\MediaTypeInterface[]
   *   An array of MediaType entities, keyed by their machine name.
   */
  private function getMediaTypesForSourcePlugins(string $media_source_plugin): array {
    $media_types = $this->entity_type_manager->getStorage('media_type')
      ->loadByProperties(['source' => $media_source_plugin]);
    ksort($media_types);
    return $media_types;
  }

  /**
   * Upcast a property name to an array mapping field names to property names.
   *
   * If there's only one media type ID, the returned array will contain a single
   * entry mapping the single field name to the given property name.
   * If there are multiple media type IDs, the returned array will contain an
   * entry for each field name, all mapping to the same given property name.
   *
   * @param string $prop_name
   *   The property name to upcast.
   * @param array $media_type_ids
   *   The media type IDs targeted by the field instance.
   * @param array $field_names_per_media_type_id
   *   An array mapping media type IDs to their respective field names.
   */
  private function upcastPropNameToArray(string $prop_name, array $media_type_ids, array $field_names_per_media_type_id): array {
    return count($media_type_ids) === 1
      ? [reset($field_names_per_media_type_id) => $prop_name]
      : array_fill_keys(array_values($field_names_per_media_type_id), $prop_name);
  }

  /**
   * Sort property names by the order of their corresponding field names.
   *
   * This ensures that property names are aligned with the order of field names
   * for each media type, which is important for consistent mapping and usage.
   *
   * @param array $prop_names_per_field_name
   *   An array mapping field names to property names.
   * @param array $field_names_per_media_type_id
   *   An array mapping media type IDs to their respective field names.
   */
  private function sortPropNamesByFieldOrder(array $prop_names_per_field_name, array $field_names_per_media_type_id): array {
    $unique_field_name_per_media = array_unique(array_values($field_names_per_media_type_id));
    $prop_name_per_field_sort = function($a, $b) use ($unique_field_name_per_media) {
      $posA = array_search($a, $unique_field_name_per_media);
      $posB = array_search($b, $unique_field_name_per_media);
      return $posA - $posB;
    };
    uksort($prop_names_per_field_name, $prop_name_per_field_sort);
    return $prop_names_per_field_name;
  }

}
