<?php

declare(strict_types=1);

namespace Drupal\acquia_dam\Hook;

use Drupal\acquia_dam\Entity\MediaItemField;
use Drupal\canvas\Hook\ShapeMatchingHooks;
use Drupal\canvas\PropExpressions\StructuredData\FieldObjectPropsExpression;
use Drupal\canvas\PropExpressions\StructuredData\FieldPropExpression;
use Drupal\canvas\PropShape\CandidateStorablePropShape;
use Drupal\canvas\TypedData\BetterEntityDataDefinition;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Hook\Order\OrderAfter;

/**
 * @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 $entityTypeManager
  ) {}

  /**
   * 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().
   *
   * Extends 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('canvas_storable_prop_shape_alter',
    order: new OrderAfter(
      classesAndMethods: [
        [ShapeMatchingHooks::class, 'mediaLibraryStorablePropShapeAlter'],
      ]
    )
  )]
  public function acquiaDamStoragePropShapeAlter(CandidateStorablePropShape $storable_prop_shape): void {

    // AcquiaDAM only supports object shapes.
    if ($storable_prop_shape->shape->schema['type'] !== 'object') {
      return;
    }
    // If shape doesn't have $ref, we don't support it.
    if (!isset($storable_prop_shape->shape->schema['$ref'])) {
      return;
    }
    // Shape must be one of supported schemas.
    if (!array_key_exists($storable_prop_shape->shape->schema['$ref'], self::SCHEMA_TO_MEDIA_SOURCE_PLUGIN)) {
      return;
    }

    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.');
    }

    $media_source_plugin_id = self::SCHEMA_TO_MEDIA_SOURCE_PLUGIN[$storable_prop_shape->shape->schema['$ref']];

    // Get all Acquia DAM media types that use the given source plugin.
    $acquia_dam_media_types = $this->getMediaTypesForSourcePlugins($media_source_plugin_id);

    // For each Acquia DAM media type, create a "branch" (variation) of this
    // prop expression that specifically targets the source field of the media
    // type.
    if (!empty($acquia_dam_media_types)) {
      foreach ($acquia_dam_media_types as $media_type_id => $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.');
        $source_field_name = MediaItemField::getFieldName($source_plugin_id);
        $media_entity_type_and_bundle = BetterEntityDataDefinition::create('media', $media_type_id);

        $branch = new FieldObjectPropsExpression(
          entityType: $media_entity_type_and_bundle,
          fieldName: $source_field_name,
          delta: NULL,
          objectPropsToFieldProps: self::getObjectProps($media_source_plugin_id, $media_entity_type_and_bundle, $source_field_name),
        );

        $storable_prop_shape->fieldTypeProp = $storable_prop_shape->fieldTypeProp->withAdditionalBranch($branch);
        $storable_prop_shape->fieldInstanceSettings['handler_settings']['target_bundles'][$media_type_id] = $media_type_id;
      }
    }
  }

  /**
   * 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->entityTypeManager->getStorage('media_type')
      ->loadByProperties(['source' => $media_source_plugin]);
    ksort($media_types);
    return $media_types;
  }

  /**
   * Returns FieldObjectProp for the given Acquia DAM source plugin ID.
   *
   * @param string $media_source_plugin_id
   *   Acquia DAM media source plugin id.
   * @param \Drupal\canvas\TypedData\BetterEntityDataDefinition $media_entity_type_and_bundle
   *   Media type and bundle.
   * @param string $source_field_name
   *   Acqquia DAM source field name.
   *
   * @return non-empty-array<string, \Drupal\canvas\PropExpressions\StructuredData\FieldPropExpression|\Drupal\canvas\PropExpressions\StructuredData\ReferenceFieldPropExpression>
   */
  protected function getObjectProps(string $media_source_plugin_id, BetterEntityDataDefinition $media_entity_type_and_bundle, string $source_field_name): array {
    return match ($media_source_plugin_id) {
      'acquia_dam_asset:image' => [
        'src' => new FieldPropExpression($media_entity_type_and_bundle, $source_field_name, \NULL, 'src_with_alternate_widths'),
        'alt' => new FieldPropExpression($media_entity_type_and_bundle, $source_field_name, \NULL, 'alt'),
        'width' => new FieldPropExpression($media_entity_type_and_bundle, $source_field_name, \NULL, 'width'),
        'height' => new FieldPropExpression($media_entity_type_and_bundle, $source_field_name, \NULL, 'height'),
      ],
      default => throw new \LogicException(),
    };
  }

}
