<?php

declare(strict_types=1);

namespace Drupal\acquia_dam\Service;

use Drupal\acquia_dam\AssetUpdater;
use Drupal\acquia_dam\IntegrationLinkRegister;
use Drupal\acquia_dam\Plugin\media\Source\Asset;
use Drupal\acquia_dam_integration_links\EnhancedIntegrationLinkRegister;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Utility\Error;
use Drupal\media\MediaInterface;

/**
 * Service for batch processing integration links.
 */
class IntegrationLinksBatchProcessor {

  use StringTranslationTrait;

  /**
   * Constructs a new IntegrationLinksBatchProcessor.
   *
   * @param \Drupal\acquia_dam\IntegrationLinkRegister $integrationLinkRegister
   *   The integration link register service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\acquia_dam\AssetUpdater $assetUpdater
   *   The asset updater service.
   * @param \Drupal\acquia_dam_integration_links\EnhancedIntegrationLinkRegister $enhancedIntegrationLinkRegister
   *   The enhanced integration link registration service.
   */
  public function __construct(
    private readonly IntegrationLinkRegister $integrationLinkRegister,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly Connection $database,
    private readonly ModuleHandlerInterface $moduleHandler,
    private readonly LoggerChannelFactoryInterface $loggerFactory,
    private readonly AssetUpdater $assetUpdater,
    private readonly ?EnhancedIntegrationLinkRegister $enhancedIntegrationLinkRegister = NULL,
  ) {}

  /**
   * Process all media entities to queue integration links.
   *
   * @param int $batch_size
   *   Batch size for processing.
   * @param string|null $media_type_filter
   *   Optional media type to filter by.
   * @param callable|null $progressCallback
   *   Optional callback for progress updates.
   *
   * @return array
   *   Results with keys: total_queued, total_processed, media_types.
   */
  public function processMediaEntities(int $batch_size = 50, ?string $media_type_filter = NULL, ?callable $progressCallback = NULL): array {
    $media_types = $this->assetUpdater->getDamMediaTypes($media_type_filter);
    $total_queued = 0;
    $total_processed = 0;

    foreach ($media_types as $media_type_id) {
      $total_count = $this->getMediaCountByType($media_type_id);

      if ($total_count === 0) {
        continue;
      }

      $offset = 0;
      $batch_count = (int) ceil($total_count / $batch_size);

      while ($offset < $total_count) {
        $media_ids = $this->getMediaIdsByTypeRanged($media_type_id, $offset, $batch_size);

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

        $media_entities = $this->entityTypeManager->getStorage('media')->loadMultiple($media_ids);

        foreach ($media_entities as $media) {
          if ($this->queueIntegrationLink($media)) {
            $total_queued++;
          }
          $total_processed++;
        }

        $offset += $batch_size;

        // Progress callback.
        if ($progressCallback) {
          $current_batch = (int) min(ceil($offset / $batch_size), $batch_count);
          $percentage = round(($current_batch / $batch_count) * 100);
          $progressCallback($media_type_id, $current_batch, $batch_count, $percentage);
        }
      }
    }

    // Flush the integration link register queue to ensure items are committed.
    $this->integrationLinkRegister->destruct();

    return [
      'total_queued' => $total_queued,
      'total_processed' => $total_processed,
      'media_types' => array_keys($media_types),
    ];
  }

  /**
   * Process content entities in enhanced mode.
   *
   * @param int $batch_size
   *   Batch size for processing.
   * @param callable|null $progressCallback
   *   Optional callback for progress updates.
   *
   * @return array
   *   Results with keys: content_queued, content_processed.
   */
  public function processContentEntities(int $batch_size = 50, ?callable $progressCallback = NULL): array {
    if (!$this->isEnhancedModeEnabled()) {
      return ['content_queued' => 0, 'content_processed' => 0];
    }

    $content_entities = $this->getContentEntitiesWithDamReferences();
    $content_queued = 0;
    $content_processed = 0;

    foreach ($content_entities as $entity_type_id => $entity_count) {
      if ($entity_count === 0) {
        continue;
      }

      $offset = 0;
      $batch_count = (int) ceil($entity_count / $batch_size);

      while ($offset < $entity_count) {
        $entity_ids = $this->getContentEntityIdsRanged($entity_type_id, $offset, $batch_size);

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

        $entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple($entity_ids);

        foreach ($entities as $entity) {
          if ($entity instanceof ContentEntityInterface) {
            $queued = $this->queueContentEntity($entity);
            $content_queued += $queued;
            $content_processed++;
          }
        }

        $offset += $batch_size;

        // Progress callback.
        if ($progressCallback) {
          $current_batch = (int) min(ceil($offset / $batch_size), $batch_count);
          $percentage = round(($current_batch / $batch_count) * 100);
          $progressCallback($entity_type_id, $current_batch, $batch_count, $percentage);
        }
      }
    }

    // Flush the integration link register queue to ensure items are committed.
    $this->integrationLinkRegister->destruct();

    return [
      'content_queued' => $content_queued,
      'content_processed' => $content_processed,
    ];
  }

  /**
   * Queue a single media entity for integration link registration.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   *
   * @return bool
   *   TRUE if queued, FALSE otherwise.
   */
  protected function queueIntegrationLink(MediaInterface $media): bool {
    if (!$media->hasField('acquia_dam_asset_id') ||
      $media->get('acquia_dam_asset_id')->isEmpty()) {
      return FALSE;
    }

    $asset_id = $media->get('acquia_dam_asset_id')->asset_id;
    if (empty($asset_id)) {
      return FALSE;
    }

    // Check if integration link already exists.
    $exists = $this->database->select('acquia_dam_integration_link_tracking', 't')
      ->fields('t', ['entity_uuid'])
      ->condition('entity_uuid', $media->uuid())
      ->condition('asset_uuid', $asset_id)
      ->countQuery()
      ->execute()
      ->fetchField();

    if ($exists) {
      return FALSE;
    }

    $this->integrationLinkRegister->addIntegrationLinksList($asset_id, $media);
    return TRUE;
  }

  /**
   * Queue content entity for enhanced integration link discovery.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The content entity.
   *
   * @return int
   *   Number of integration links queued (new links only, skips existing).
   */
  protected function queueContentEntity(ContentEntityInterface $entity): int {
    if (!$this->isEnhancedModeEnabled()) {
      return 0;
    }

    try {
      if (!$this->enhancedIntegrationLinkRegister) {
        return 0;
      }

      // Collect DAM references and check for existing integration links.
      $dam_references_to_queue = [];
      $dam_reference_count = 0;

      foreach ($entity->getFields() as $field) {
        // We only need entity_reference fields that are not empty.
        if ($field->getFieldDefinition()->getType() !== 'entity_reference' || $field->isEmpty()) {
          continue;
        }
        foreach ($field->referencedEntities() as $referenced_entity) {
          // Only Media entities are supported.
          if (!$referenced_entity instanceof MediaInterface) {
            continue;
          }
          $source = $referenced_entity->getSource();
          // Only Acquia DAM assets are supported.
          if (!$source instanceof Asset) {
            continue;
          }
          // Only process if the referenced media has an Acquia DAM asset ID.
          if (!$referenced_entity->hasField('acquia_dam_asset_id')) {
            continue;
          }
          // If the field exists but is empty, skip.
          if ($referenced_entity->get('acquia_dam_asset_id')->isEmpty()) {
            continue;
          }
          $asset_id = $referenced_entity->get('acquia_dam_asset_id')->asset_id;
          // If the asset ID is empty, skip.
          if (empty($asset_id)) {
            continue;
          }
          // Check if integration link already exists
          // for this content entity and asset.
          $exists = $this->database->select('acquia_dam_integration_link_tracking', 't')
            ->fields('t', ['entity_uuid'])
            ->condition('entity_uuid', $entity->uuid())
            ->condition('asset_uuid', $asset_id)
            ->countQuery()
            ->execute()
            ->fetchField();

          if (!$exists) {
            $dam_references_to_queue[] = $referenced_entity;
            $dam_reference_count++;
          }
        }
      }

      // Only track asset usage if there are new references to queue.
      if (!empty($dam_references_to_queue)) {
        $this->enhancedIntegrationLinkRegister->trackAssetUsage($entity);
      }

      return $dam_reference_count;
    }
    catch (\Exception $e) {
      $error_context = Error::decodeException($e);
      $this->loggerFactory->get('acquia_dam')->error('Error queueing content entity: @message in @file on line @line', $error_context);
      return 0;
    }
  }

  /**
   * Get media count by media type.
   *
   * @param string $media_type_id
   *   The media type ID.
   *
   * @return int
   *   Count of media entities.
   */
  protected function getMediaCountByType(string $media_type_id): int {
    return (int) $this->entityTypeManager->getStorage('media')
      ->getQuery()
      ->condition('bundle', $media_type_id)
      ->accessCheck(FALSE)
      ->count()
      ->execute();
  }

  /**
   * Get media IDs by media type using ranged query.
   *
   * @param string $media_type_id
   *   The media type ID.
   * @param int $offset
   *   Query offset.
   * @param int $limit
   *   Query limit.
   *
   * @return array
   *   Array of media entity IDs.
   */
  protected function getMediaIdsByTypeRanged(string $media_type_id, int $offset, int $limit): array {
    return $this->entityTypeManager->getStorage('media')
      ->getQuery()
      ->condition('bundle', $media_type_id)
      ->accessCheck(FALSE)
      ->range($offset, $limit)
      ->execute();
  }

  /**
   * Check if enhanced mode is enabled.
   *
   * @return bool
   *   TRUE if acquia_dam_integration_links module is enabled.
   */
  public function isEnhancedModeEnabled(): bool {
    return $this->moduleHandler->moduleExists('acquia_dam_integration_links');
  }

  /**
   * Get content entities with DAM references.
   *
   * @return array
   *   Array of entity counts keyed by entity type ID.
   */
  protected function getContentEntitiesWithDamReferences(): array {
    $entities = [];

    // Dynamically discover entity types with DAM references.
    $entity_types = $this->discoverEntityTypesWithDamReferences();

    foreach ($entity_types as $entity_type_id) {
      try {
        $storage = $this->entityTypeManager->getStorage($entity_type_id);
        $count = (int) $storage->getQuery()
          ->accessCheck(FALSE)
          ->count()
          ->execute();

        if ($count > 0) {
          $entities[$entity_type_id] = $count;
        }
      }
      catch (\Exception $e) {
        $error_context = Error::decodeException($e);
        $this->loggerFactory->get('acquia_dam')->error('Error fetching content entities with DAM references: @message in @file on line @line', $error_context);
        continue;
      }
    }

    return $entities;
  }

  /**
   * Discover entity types that have fields referencing DAM media.
   *
   * This method dynamically discovers all content entity types that have
   * entity_reference fields targeting media entities.
   *
   * @return array
   *   Array of entity type IDs that have media reference fields.
   */
  protected function discoverEntityTypesWithDamReferences(): array {
    $entity_types_with_references = [];

    // Get all entity type definitions.
    $entity_type_definitions = $this->entityTypeManager->getDefinitions();

    // Get all DAM media type IDs to check against.
    $dam_media_types = array_keys($this->assetUpdater->getDamMediaTypes());

    foreach ($entity_type_definitions as $entity_type_id => $entity_type) {
      // Only process content entities (fieldable entities).
      if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) {
        continue;
      }

      try {
        // Get field storage definitions for this entity type.
        $field_storage_definitions = $this->entityTypeManager
          ->getStorage('field_storage_config')
          ->loadByProperties(['entity_type' => $entity_type_id]);

        foreach ($field_storage_definitions as $field_storage) {
          // Only check entity_reference fields.
          if ($field_storage->getType() !== 'entity_reference') {
            continue;
          }

          // Check if this field targets media entities.
          $target_type = $field_storage->getSetting('target_type');
          if ($target_type !== 'media') {
            continue;
          }

          // Load field instances for this storage to check handler_settings.
          $field_configs = $this->entityTypeManager
            ->getStorage('field_config')
            ->loadByProperties([
              'entity_type' => $entity_type_id,
              'field_name' => $field_storage->getName(),
            ]);

          // Check each field instance for target bundle restrictions.
          foreach ($field_configs as $field_config) {
            $handler_settings = $field_config->getSetting('handler_settings');
            $target_bundles = $handler_settings['target_bundles'] ?? [];

            // If no target bundles specified, it can reference any media type.
            // If target bundles are specified,
            // check onee of it is DAM media types.
            if (empty($target_bundles) || !empty(array_intersect($target_bundles, $dam_media_types))) {
              // Found at least one field that can reference DAM media.
              $entity_types_with_references[] = $entity_type_id;
              // No need to check other fields on this entity type.
              // Break out of both field_configs and field_storage loops.
              break 2;
            }
          }
        }
      }
      catch (\Exception $e) {
        $error_context = Error::decodeException($e);
        $this->loggerFactory->get('acquia_dam')->error('Error discovering entity types with DAM references for @entity_type: @message in @file on line @line', $error_context + ['@entity_type' => $entity_type_id]);
        continue;
      }
    }

    return $entity_types_with_references;
  }

  /**
   * Get content entity IDs using ranged query.
   *
   * @param string $entity_type_id
   *   The entity type ID.
   * @param int $offset
   *   Query offset.
   * @param int $limit
   *   Query limit.
   *
   * @return array
   *   Array of entity IDs.
   */
  protected function getContentEntityIdsRanged(string $entity_type_id, int $offset, int $limit): array {
    try {
      return $this->entityTypeManager->getStorage($entity_type_id)
        ->getQuery()
        ->accessCheck(FALSE)
        ->range($offset, $limit)
        ->execute();
    }
    catch (\Exception $e) {
      $error_context = Error::decodeException($e);
      $this->loggerFactory->get('acquia_dam')->error('Error fetching content entity IDs: @message in @file on line @line', $error_context);
      return [];
    }
  }

}
