<?php

namespace Drupal\lingotek\Plugin\QueueWorker;

use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\Queue\RequeueException;
use Drupal\Core\Queue\SuspendQueueException;
use Drupal\lingotek\Exception\LingotekApiException;
use Drupal\lingotek\Exception\LingotekContentEntityStorageException;
use Drupal\Component\Render\FormattableMarkup;

/**
 * Provides base functionality for the Lingotek Queue Workers.
 *
 * @QueueWorker(
 *   id = "lingotek_downloader_queue_worker",
 *   title = @Translation("Lingotek Download Queue"),
 *   cron = {"time" = 60}
 * )
 */
class LingotekDownloaderQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    $locale = $data['locale'];
    $entity_type_id = $data['entity_type_id'];
    $entity_id = $data['entity_id'];
    $document_id = $data['document_id'];
    $partition_key = $data['partition_key'] ?? ($entity_type_id . ':' . $entity_id);
    $max_attempts = (int) ($data['max_attempts'] ?? 3);
    $attempt_store = \Drupal::keyValue('lingotek_download_attempts');
    $attempt_key = $this->getAttemptKey($data);

    $partition_lock_name = 'lingotek_queue_partition:' . $partition_key;
    $lock = \Drupal::lock();
    if (!$lock->acquire($partition_lock_name, 900.0)) {
      // Another worker is mid-download for this entity. Block briefly for
      // them to finish (typical entity save: 1-10s). $lock->wait() returns
      // FALSE when the lock becomes available within $delay and TRUE when
      // $delay is exhausted; per Drupal core's LockBackendInterface
      // docblock example, the correct idiom is to ignore the return value
      // and just try acquire() again. If the second acquire still fails,
      // suspend the queue so the next cron cycle retries from a clean
      // slate, instead of burning RequeueException retries in <5ms while
      // the lock holder is still working.
      $lock->wait($partition_lock_name, 30.0);
      if (!$lock->acquire($partition_lock_name, 900.0)) {
        \Drupal::logger('lingotek')->warning('Partition lock still busy after wait for %partition while processing %locale (%entity_type:%entity_id); suspending queue until next cron cycle.', [
          '%partition' => $partition_key,
          '%locale' => $locale,
          '%entity_type' => $entity_type_id,
          '%entity_id' => $entity_id,
        ]);
        throw new SuspendQueueException(sprintf('Lingotek download queue suspended: partition lock still busy for %s.', $partition_key));
      }
    }

    try {
      $source = $data['context']['source'] ?? 'unknown';
      \Drupal::logger('lingotek')->info('Queued download started: job_id=@job document=@doc locale=@loc entity=@type:@id source=@src', [
        '@job' => $data['job_id'] ?? '',
        '@doc' => $document_id,
        '@loc' => $locale,
        '@type' => $entity_type_id,
        '@id' => (string) $entity_id,
        '@src' => $source,
      ]);
      $entity = \Drupal::entityTypeManager()
        ->getStorage($entity_type_id)
        ->load($entity_id);
      if ($entity instanceof ConfigEntityInterface) {
        /** @var \Drupal\lingotek\LingotekConfigTranslationServiceInterface $translation_service */
        $translation_service = \Drupal::service('lingotek.config_translation');
      }
      elseif ($entity instanceof ContentEntityInterface) {
        /** @var \Drupal\lingotek\LingotekContentTranslationServiceInterface $translation_service */
        $translation_service = \Drupal::service('lingotek.content_translation');
      }
      if (empty($translation_service)) {
        \Drupal::logger('lingotek')
          ->error('Can not download - entity (@instance) is not supported instance of a class', [
              '@instance' => gettype($entity),
            ]
          );
        return;
      }
      $download = $translation_service->downloadDocument($entity, $locale);

      if ($download === FALSE || $download === NULL) {
        $this->retryOrFail($attempt_store, $attempt_key, $max_attempts, 'No download for target @locale happened in document @document on @entity @bundle @id.', [
          '@locale' => $locale,
          '@document' => $document_id,
          '@entity' => $entity->label(),
          '@id' => $entity->id(),
          '@bundle' => $entity->bundle(),
        ]);
      }
      else {
        $attempt_store->delete($attempt_key);
      }
    }
    catch (LingotekContentEntityStorageException $storage_exception) {
      if ($storage_exception->isLockTimeout()) {
        $this->retryOrFail($attempt_store, $attempt_key, $max_attempts, 'Retrying queued download for @locale in @document after lock timeout.', [
          '@locale' => $locale,
          '@document' => $document_id,
        ]);
        return;
      }
      $attempt_store->delete($attempt_key);
      \Drupal::logger('lingotek')->error('Queued download failed with non-retryable storage exception for @locale in @document: @message', [
        '@locale' => $locale,
        '@document' => $document_id,
        '@message' => $storage_exception->getMessage(),
      ]);
    }
    catch (LingotekApiException $exception) {
      $this->retryOrFail($attempt_store, $attempt_key, $max_attempts, 'Retrying queued download for @locale in @document after Lingotek API failure: @message', [
        '@locale' => $locale,
        '@document' => $document_id,
        '@message' => $exception->getMessage(),
      ]);
    }
    catch (RequeueException $exception) {
      throw $exception;
    }
    catch (\Exception $exception) {
      $this->retryOrFail($attempt_store, $attempt_key, $max_attempts, 'Retrying queued download for @locale in @document after generic failure: @message', [
        '@locale' => $locale,
        '@document' => $document_id,
        '@message' => $exception->getMessage(),
      ]);
    }
    finally {
      $lock->release($partition_lock_name);
    }
  }

  /**
   * Retry queued work item up to max attempts; then mark permanent failure.
   */
  protected function retryOrFail($attempt_store, string $attempt_key, int $max_attempts, string $message, array $context = []) {
    $attempt = (int) $attempt_store->get($attempt_key, 0) + 1;
    if ($attempt < $max_attempts) {
      $attempt_store->set($attempt_key, $attempt);
      $context['@attempt'] = $attempt;
      $context['@max'] = $max_attempts;
      \Drupal::logger('lingotek')->warning($message . ' (attempt @attempt/@max)', $context);
      throw new RequeueException((string) new FormattableMarkup($message, $context));
    }
    $attempt_store->delete($attempt_key);
    \Drupal::logger('lingotek')->error('Queued download failed after maximum retries (%max): @reason', [
      '%max' => $max_attempts,
      '@reason' => (string) new FormattableMarkup($message, $context),
    ]);
  }

  /**
   * Builds a stable key used to track retry attempts.
   */
  protected function getAttemptKey(array $data): string {
    if (!empty($data['job_id'])) {
      return (string) $data['job_id'];
    }
    return sha1(($data['entity_type_id'] ?? '') . ':' . ($data['entity_id'] ?? '') . ':' . ($data['locale'] ?? '') . ':' . ($data['document_id'] ?? ''));
  }

}
