<?php

declare(strict_types=1);

namespace Drupal\migrate_leshed\Plugin\migrate\process;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Copies a local image, downscaling it to a web-friendly size, as a File.
 *
 * The source value is a readable file path/URI. The image is scaled down
 * (aspect ratio preserved, never upscaled) to fit within max_width x
 * max_height, saved into the destination directory and registered as a
 * managed file. Returns the file id.
 *
 * Configuration:
 * - destination: (required) destination directory URI, e.g. 'public://projets/1/'.
 * - max_width: maximum width in pixels (default 1920).
 * - max_height: maximum height in pixels (default 1920).
 * - uid: owner uid of the created file (default 0).
 * - reuse: reuse an existing managed file at the destination URI (default TRUE).
 * - skip_on_missing_source: skip the value when the source is missing
 *   (default TRUE) instead of failing.
 *
 * @code
 * field_images/target_id:
 *   plugin: image_resize_import
 *   source: uri
 *   destination: destination
 *   max_width: 1920
 *   max_height: 1920
 * @endcode
 */
#[MigrateProcess(id: 'image_resize_import')]
final class ImageResizeImport extends ProcessPluginBase implements ContainerFactoryPluginInterface {

  /**
   * Per-run cache of already resolved destination URIs to file ids.
   *
   * @var array<string, int>
   */
  private array $fidCache = [];

  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    private readonly FileSystemInterface $fileSystem,
    private readonly ImageFactory $imageFactory,
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {
    $configuration += [
      'max_width' => 1920,
      'max_height' => 1920,
      'uid' => 0,
      'reuse' => TRUE,
      'skip_on_missing_source' => TRUE,
    ];
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
    return new self(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('file_system'),
      $container->get('image.factory'),
      $container->get('entity_type.manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): ?int {
    $source = is_string($value) ? trim($value) : '';
    if ($source === '') {
      return NULL;
    }

    // Stream wrappers support is_file() directly; realpath() is only needed as
    // a fallback for schemes it cannot resolve.
    if (!is_file($source) && !is_file($this->fileSystem->realpath($source) ?: $source)) {
      // Skipping throws MigrateSkipRowException because, inside a sub_process,
      // only that exception drops the whole image item cleanly (SubProcess
      // catches it and `continue`s); MigrateSkipProcessException would instead
      // leave a broken delta with alt/caption but no target_id.
      return $this->skipImage($migrate_executable, "Source image '$source' is missing.");
    }

    $directory = $this->getPropertyValue($this->configuration['destination'], $row);
    if (!is_string($directory) || $directory === '') {
      return $this->skipImage($migrate_executable, "Missing 'destination' for image '$source'.");
    }
    $directory = rtrim($directory, '/') . '/';
    $destination = $directory . $this->fileSystem->basename($source);

    // Reuse an already imported file (idempotency), memoized for the run.
    if ($this->configuration['reuse']) {
      if (isset($this->fidCache[$destination])) {
        return $this->fidCache[$destination];
      }
      $existing = $this->entityTypeManager->getStorage('file')
        ->loadByProperties(['uri' => $destination]);
      if ($existing) {
        return $this->fidCache[$destination] = (int) reset($existing)->id();
      }
    }

    // Any infrastructure failure only drops the current image (logged), so a
    // single unreadable/oversized file never aborts the whole batch.
    try {
      if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
        return $this->skipImage($migrate_executable, "Cannot prepare directory '$directory'.");
      }

      $image = $this->imageFactory->get($source);
      if (!$image->isValid()) {
        return $this->skipImage($migrate_executable, "Source '$source' is not a valid image.");
      }
      // Downscale only: scale() never upscales when upscale is FALSE.
      $image->scale((int) $this->configuration['max_width'], (int) $this->configuration['max_height']);
      if (!$image->save($destination)) {
        return $this->skipImage($migrate_executable, "Failed to write resized image to '$destination'.");
      }

      $file = $this->entityTypeManager->getStorage('file')->create([
        'uri' => $destination,
        'uid' => (int) $this->getPropertyValue($this->configuration['uid'], $row),
        'status' => 1,
      ]);
      $file->save();
    }
    catch (\Throwable $e) {
      return $this->skipImage($migrate_executable, "Import of image '$source' failed: " . $e->getMessage());
    }

    return $this->fidCache[$destination] = (int) $file->id();
  }

  /**
   * Logs a message and skips the current image item.
   *
   * @throws \Drupal\migrate\MigrateSkipRowException
   */
  private function skipImage(MigrateExecutableInterface $migrate_executable, string $message): never {
    $migrate_executable->saveMessage($message);
    throw new MigrateSkipRowException($message, FALSE);
  }

  /**
   * Resolves a configuration value, supporting destination (@) references.
   */
  private function getPropertyValue(mixed $key, Row $row): mixed {
    if (is_string($key) && str_starts_with($key, '@')) {
      return $row->get(substr($key, 1));
    }
    if (is_string($key) && $row->hasSourceProperty($key)) {
      return $row->getSourceProperty($key);
    }

    return $key;
  }

}
