<?php

declare(strict_types=1);

namespace Drupal\acquia_dam;

use Drupal\acquia_dam\Entity\MediaSourceField;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\media\MediaInterface;

/**
 * Service for processing Acquia DAM asset updates.
 */
final class AssetUpdater {

  use StringTranslationTrait;

  /**
   * Constructs a new AssetUpdateProcessor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\acquia_dam\AssetUpdateChecker $assetUpdateChecker
   *   DAM asset update checker.
   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
   *   The queue factory.
   * @param \Drupal\Core\Queue\QueueWorkerManagerInterface $queueManager
   *   The queue worker manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   */
  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly AssetUpdateChecker $assetUpdateChecker,
    private readonly AssetVersionResolver $assetVersionResolver,
    private readonly QueueFactory $queueFactory,
    private readonly QueueWorkerManagerInterface $queueManager,
    private readonly LoggerChannelFactoryInterface $loggerFactory
  ) {}

  /**
   * Queue all DAM media items for update checking.
   *
   * @param array $options
   *   Options array containing:
   *   - batch-size: Number of items to process per batch
   *   - media-type: Specific media type to process (optional)
   *
   * @return array
   *   Summary array with totals for processed, updated, and errors.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function queueAssets(array $options = []): array {
    $batch_size = (int) ($options['batch-size'] ?? 50);
    $media_type_filter = $options['media-type'] ?? NULL;
    $finalized_version = (bool) $options['finalized-version'] ?? FALSE;

    $media_types = $this->getDamMediaTypes($media_type_filter);

    if (empty($media_types)) {
      return [
        'media_types' => [],
        'total_processed' => 0,
        'total_updated' => 0,
        'total_errors' => 0,
        'message' => 'No DAM media types found.',
      ];
    }

    $total_processed = 0;
    $total_updated = 0;
    $total_errors = 0;

    foreach ($media_types as $media_type) {
      $result = $this->processMediaType($media_type, $batch_size, $finalized_version);
      $total_processed += $result['processed'];
      $total_updated += $result['updated'];
      $total_errors += $result['errors'];
    }

    return [
      'media_types' => $media_types,
      'total_processed' => $total_processed,
      'total_updated' => $total_updated,
      'total_errors' => $total_errors,
      'message' => 'Assets queued successfully.',
    ];
  }

  /**
   * Process the Acquia DAM asset update queue.
   *
   * @param array $options
   *   Options array containing:
   *   - limit: Maximum number of queue items to process (0 = no limit)
   *
   * @return array
   *   Summary array with processing results.
   */
  public function processQueue(array $options = []): array {
    $limit = (int) ($options['limit'] ?? 0);

    $queue = $this->queueFactory->get('acquia_dam_media_item_update');
    $queue_worker = $this->queueManager->createInstance('acquia_dam_media_item_update');

    $processed = 0;
    $errors = 0;
    $initial_count = $queue->numberOfItems();

    while (($limit == 0 || $processed < $limit) && ($item = $queue->claimItem())) {
      try {
        $queue_worker->processItem($item->data);
        $queue->deleteItem($item);
        $processed++;
      }
      catch (\Exception $e) {
        $errors++;
        $this->loggerFactory->get('acquia_dam')->error(
          'Error processing queue item: @message',
          ['@message' => $e->getMessage()]
        );
        // Release the item back to the queue on error
        $queue->releaseItem($item);
      }
    }

    return [
      'initial_count' => $initial_count,
      'processed' => $processed,
      'errors' => $errors,
      'remaining' => $queue->numberOfItems(),
      'message' => 'Queue processing completed.',
    ];
  }

  /**
   * Process media items of a specific type.
   *
   * @param string $media_type
   *   The media type to process.
   * @param int $batch_size
   *   Number of items to process per batch.
   * @param bool $finalized_version
   *   Whether to check for finalized versions.
   * @param callable|null $progress_callback
   *   Optional callback for progress reporting.
   *
   * @return array
   *   Array with counts of processed, updated, and errors.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function processMediaType(string $media_type, int $batch_size, bool $finalized_version = false, ?callable $progress_callback = NULL): array {
    $media_storage = $this->entityTypeManager->getStorage('media');

    // Get total count for progress tracking
    $total_query = $media_storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('bundle', $media_type)
      ->condition(MediaSourceField::SOURCE_FIELD_NAME . '.asset_id', NULL, 'IS NOT NULL');

    $total_count = $total_query->count()->execute();

    if ($total_count == 0) {
      if ($progress_callback) {
        $progress_callback([
          'type' => 'info',
          'message' => "No media items found for type: $media_type",
        ]);
      }
      return ['processed' => 0, 'updated' => 0, 'errors' => 0];
    }

    if ($progress_callback) {
      $progress_callback([
        'type' => 'info',
        'message' => "Found $total_count media items to process for type: $media_type",
      ]);
    }

    $processed = 0;
    $updated = 0;
    $errors = 0;
    $offset = 0;

    // Process in batches
    while ($offset < $total_count) {
      $batch_query = $media_storage->getQuery()
        ->accessCheck(FALSE)
        ->condition('bundle', $media_type)
        ->condition(MediaSourceField::SOURCE_FIELD_NAME . '.asset_id', NULL, 'IS NOT NULL')
        ->sort('mid')
        ->range($offset, $batch_size);

      $media_ids = $batch_query->execute();

      if (empty($media_ids)) {
        break;
      }

      /** @var \Drupal\media\MediaInterface[] $media_items */
      $media_items = $media_storage->loadMultiple($media_ids);

      foreach ($media_items as $media_item) {
        $processed++;

        try {
          $progress = sprintf('[%d/%d]', $processed, $total_count);

          if ($finalized_version) {
            $check_result = $this->checkAssetsFinilizedVersion($media_item);
          }
          else {
            // Check if the asset needs updating (this will queue updates if needed)
            $check_result = $this->assetUpdateChecker->checkAssets($media_item, 1);
          }

          if ($check_result === FALSE) {
            // FALSE means update is needed and item was queued
            $updated++;
            if ($progress_callback) {
              $progress_callback([
                'type' => 'success',
                'message' => "$progress Queued update for media item: {$media_item->label()} (ID: {$media_item->id()})",
              ]);
            }
          }
          elseif ($check_result === TRUE && !$finalized_version) {
            // TRUE means no update needed, but item was still queued for verification
            $updated++;
            if ($progress_callback) {
              $progress_callback([
                'type' => 'comment',
                'message' => "$progress Queued verification for media item: {$media_item->label()} (ID: {$media_item->id()}) - may not need update",
              ]);
            }
          }
          else {
            // NULL means error occurred
            if ($progress_callback) {
              $progress_callback([
                'type' => 'error',
                'message' => "$progress Error checking media item: {$media_item->label()} (ID: {$media_item->id()})",
              ]);
            }
          }
        }
        catch (\Exception $e) {
          $errors++;
          $error_message = "Error processing media item {$media_item->id()}: {$e->getMessage()}";

          if ($progress_callback) {
            $progress_callback([
              'type' => 'error',
              'message' => $error_message,
            ]);
          }

          $this->loggerFactory->get('acquia_dam')->error(
            'Error processing media item @id: @message',
            [
              '@id' => $media_item->id(),
              '@message' => $e->getMessage(),
            ]
          );
        }
      }

      $offset += $batch_size;

      // Show progress
      if ($processed % ($batch_size * 2) == 0 || $processed >= $total_count) {
        if ($progress_callback) {
          $progress_callback([
            'type' => 'progress',
            'message' => "Progress: $processed/$total_count items processed",
          ]);
        }
      }
    }

    return [
      'processed' => $processed,
      'updated' => $updated,
      'errors' => $errors,
    ];
  }

  /**
   * Get all media types that use DAM assets.
   *
   * @param string|null $media_type_filter
   *   Optional filter for a specific media type.
   *
   * @return array
   *   Array of media type IDs that use DAM assets.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function getDamMediaTypes(?string $media_type_filter = NULL): array {
    $media_type_storage = $this->entityTypeManager->getStorage('media_type');

    // Find all media types that use DAM assets.
    $media_types = $media_type_storage->loadByProperties([
      'source_configuration.source_field' => MediaSourceField::SOURCE_FIELD_NAME,
    ]);

    // Extract media type IDs using array_map.
    $media_types = array_map(fn($media_type) => $media_type->id(), $media_types);

    // Return filtered media type or all media types.
    return $media_type_filter && isset($media_types[$media_type_filter])
      ? [$media_type_filter => $media_type_filter]
      : $media_types;
  }

  /**
   * Check if the asset is finalized version.
   *
   * @param \Drupal\media\MediaInterface $media_item
   *   The media item to check.
   * @param int $user_id
   *   The user id.
   *
   * @return bool
   *   TRUE if the asset is finalized, FALSE otherwise.
   */
  public function checkAssetsFinilizedVersion(MediaInterface $media_item, int $user_id = 0): bool {
    assert($media_item instanceof MediaInterface);
    $asset_field = $media_item->get(MediaSourceField::SOURCE_FIELD_NAME)->first();
    if (!$asset_field || is_null($asset_field->getValue()['asset_id'])) {
      $this->loggerFactory->get('acquia_dam')->error($this->t('Media item with ID %media_item_id has no asset ID set, which normally should not happen.', [
        '%media_item_id' => $media_item->id(),
      ]), ['link' => $media_item->toLink($this->t('View'))->toString()]);
      return FALSE;
    }

    $asset_ids = $asset_field->getValue();
    // Key turning point deciding whether the media item is up-to-date or not.
    if ($asset_ids['version_id'] === $this->assetVersionResolver->getFinalizedVersion($asset_ids['asset_id'])) {
      return TRUE;
    }

    // Now if updating is needed, then queue the task for later processing.
    $this->queueFactory->get('acquia_dam_media_item_update')->createItem([
      'asset_id' => $asset_ids['asset_id'],
      'media_id' => $media_item->id(),
      'user_id' => $user_id,
    ]);

    return FALSE;
  }

}
