<?php

declare(strict_types=1);

namespace Drupal\migrate_leshed\Plugin\migrate\process;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
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 file into the file system as a managed File entity.
 *
 * The source value is a readable file path/URI. The file is copied into the
 * destination directory and registered as a managed file; the file id is
 * returned.
 *
 * Configuration:
 * - destination: (required) destination directory URI, e.g. 'public://2026-08/'.
 * - 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_fichiers/target_id:
 *   plugin: file_import
 *   source: uri
 *   destination: destination
 * @endcode
 */
#[MigrateProcess(id: 'file_import')]
final class FileImport 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 EntityTypeManagerInterface $entityTypeManager,
  ) {
    $configuration += [
      '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('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;
    }

    if (!is_file($source) && !is_file($this->fileSystem->realpath($source) ?: $source)) {
      return $this->skipFile($migrate_executable, "Source file '$source' is missing.");
    }

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

    // Reuse an already imported file to keep the migration idempotent.
    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();
      }
    }

    try {
      if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
        return $this->skipFile($migrate_executable, "Cannot prepare directory '$directory'.");
      }
      $this->fileSystem->copy($source, $destination, FileExists::Replace);

      $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->skipFile($migrate_executable, "Import of file '$source' failed: " . $e->getMessage());
    }

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

  /**
   * Logs a message and skips the current file item.
   *
   * @throws \Drupal\migrate\MigrateSkipRowException
   */
  private function skipFile(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;
  }

}
