<?php

namespace Drupal\acquia_contenthub\Commands;

use Acquia\ContentHubClient\Syndication\SyndicationState;
use Consolidation\AnnotatedCommand\CommandError;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\Exception\QueueItemInputViolationException;
use Drupal\acquia_contenthub\Libs\Common\EnsureContentHubClientTrait;
use Drupal\acquia_contenthub\Libs\PullSyndication\QueueItemInputValidator;
use Drupal\Component\Serialization\Json;
use Drush\Commands\DrushCommands;
use Symfony\Component\Console\Helper\Table;

/**
 * Drush commands for service queue operations.
 */
class AcquiaContentHubSyndicationQueueCommands extends DrushCommands {

  use EnsureContentHubClientTrait;

  /**
   * Client Factory.
   *
   * @var \Drupal\acquia_contenthub\Client\ClientFactory
   */
  protected ClientFactory $clientFactory;

  /**
   * AcquiaContentHubSyndicationQueueCommands constructor.
   *
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $client_factory
   *   ACH client factory.
   */
  public function __construct(ClientFactory $client_factory) {
    $this->clientFactory = $client_factory;
  }

  /**
   * Validates parameters for the syndication queue operations.
   *
   * @hook validate acquia:contenthub:queues:syndications
   */
  public function validateSyndicationQueueOperation(): ?CommandError {
    $op = $this->input()->getArgument('op');
    switch ($op) {
      case 'update':
        return $this->validateUpdateOperation();

      case 'delete':
        return $this->validateDeleteOperation();

      case 'describe':
        return $this->validateDescribeOperation();

      case 'list':
        return $this->validateListOperation();

      default:
        return new CommandError('Invalid operation. Use "update", "delete", "describe" or "list".', self::EXIT_FAILURE);
    }
  }

  /**
   * Validates List operation parameters.
   */
  private function validateListOperation(): ?CommandError {
    $state = $this->input()->getOption('state');

    if (!empty($state) && !SyndicationState::isValidState($state)) {
      $valid_states_list = implode(', ', SyndicationState::getAll());
      return new CommandError("Invalid state '$state' provided. Valid states are: $valid_states_list", self::EXIT_FAILURE);
    }
    return NULL;
  }

  /**
   * Validates describe operation parameters.
   */
  private function validateDescribeOperation(): ?CommandError {
    $id = $this->input()->getOption('syndication-id');

    if (empty($id)) {
      return new CommandError('--syndication-id is required for describe operation.', self::EXIT_FAILURE);
    }

    return NULL;
  }

  /**
   * Validates update operation parameters.
   */
  private function validateUpdateOperation(): ?CommandError {
    $queue_id = $this->input()->getOption('syndication-id');
    $state = $this->input()->getOption('state');
    $visibility_timeout = $this->input()->getOption('visibility-timeout');
    $reason = $this->input()->getOption('reason');

    if (empty($queue_id)) {
      return new CommandError('The --syndication-id option is required for update operation.', self::EXIT_FAILURE);

    }
    if (empty($state) && empty($visibility_timeout) && empty($reason)) {
      return new CommandError('Update requires at least one of --state, --visibility-timeout, or --reason flags.', self::EXIT_FAILURE);
    }

    if (!empty($state) && !SyndicationState::isValidState($state)) {
      $valid_states_list = implode(', ', SyndicationState::getAll());
      return new CommandError("Invalid state '$state' provided. Valid states are: $valid_states_list", self::EXIT_FAILURE);
    }

    if (!empty($visibility_timeout)) {
      try {
        QueueItemInputValidator::validateVisibilityTimeout($visibility_timeout);
      }
      catch (QueueItemInputViolationException $exception) {
        return new CommandError($exception->getMessage(), self::EXIT_FAILURE);
      }
    }

    return NULL;
  }

  /**
   * Validates delete operation parameters.
   */
  private function validateDeleteOperation(): ?CommandError {
    $all = $this->input()->getOption('all');
    $ids = $this->input()->getOption('syndication-ids');
    $uuids = $this->input()->getOption('entity-uuids');

    if (empty($all) && empty($ids) && empty($uuids)) {
      return new CommandError('Provide at least one of --all, --syndication-ids, or --entity-uuids for delete operation.', self::EXIT_FAILURE);
    }

    if (!empty($ids)) {
      $syndication_ids = explode(',', $ids);
      foreach ($syndication_ids as $id) {
        if (!is_numeric(trim($id))) {
          return new CommandError("Invalid syndication-id format: '$id'. Syndication IDs must be numeric.", self::EXIT_FAILURE);
        }
      }
    }
    return NULL;
  }

  /**
   * Performs operations on Service queues.
   *
   * @param string $op
   *   The list|describe|update|delete operation.
   *
   * @option all
   *   (delete) Refers to all the items in service queue.
   * @default all false
   * @option syndication-ids
   *   (delete,list) Comma-separated --syndication-ids from service queue.
   * @default syndication-ids ''
   * @option entity-uuids
   *   (delete,list) Comma-separated --entity-uuids from service queue.
   * @default entity-uuids ''
   * @option syndication-id
   *   (describe,update) Syndication-id from service queue.
   * @default syndication-id ''
   * @option visibility-timeout
   *   (update) Visibility timeout for entity.
   *   Accepted units: ns, us, µs, ms, s, m, h (e.g., 300s, 5m, 1h).
   * @default visibility-timeout ''
   * @option state
   *   (list,update) State for update operation.
   *   Accepted values: processing, failed, queued, processed, pending.
   * @default state ''
   * @option reason
   *   (update) The reason for the particular entity being added to the queue,
   *   e.g. interest list, filters, dependency etc.
   * @default reason ''
   * @option json
   *   (describe,list) Provides output in JSON format.
   * @default json false
   * @option client-uuids
   *   (list) Filters output using client-uuids, comma-separated list.
   * @default client-uuids ''
   * @option from
   *   (list) The value of offset; used for pagination.
   * @default from 0
   * @option size
   *   (list) The value of page size; used for pagination.
   * @default size 3000
   * @option add-cdf-details
   *   (describe) Adds CDF of the requested item in the output, converts format
   *   to json for easier readability.
   * @default add-cdf-details false
   *
   * @command acquia:contenthub:queues:syndications
   * @aliases ach-qs
   *
   * @usage drush acquia:contenthub:queues:syndications delete --all
   *   Deletes all the items in service queue.
   * @usage drush acquia:contenthub:queues:syndications delete --syndication-ids='1,2'
   *   Deletes items using syndication-ids from service queue.
   * @usage drush acquia:contenthub:queues:syndications delete --entity-uuids='1c6f7a8a-5cf6-4bb2-8e09-e64c0d63629d,4c6f7a8a-5cf6-4bb2-8e09-e64c0d63629e'
   *   Deletes items using entity-uuids from service queue.
   * @usage drush acquia:contenthub:queues:syndications update --syndication-id=1 --state=pending --visibility-timeout=300s --reason='Update reason'
   *   Updates single item for provided syndication-id from service queue.
   * @usage drush acquia:contenthub:queues:syndications list --syndication-ids=1,2
   *   Fetches items from service queue and displays in tabular format.
   * @usage drush acquia:contenthub:queues:syndications list --state=queued
   *   Adds state filter while fetching items from service queue.
   * @usage drush acquia:contenthub:queues:syndications list --json
   *   Fetches items from service queue and displays in JSON format.
   * @usage drush acquia:contenthub:queues:syndications describe --syndication-id=1
   *   Fetches single item for provided syndication-id from service queue
   *   and displays in tabular format.
   *
   * @throws \Exception
   */
  public function queueSyndication(string $op): int {
    switch ($op) {
      case 'update':
        return $this->updateItemInServiceQueue();

      case 'delete':
        return $this->deleteItemsFromServiceQueue();

      case 'list':
        return $this->listItemsFromServiceQueue();

      case 'describe':
        return $this->describeServiceQueueItem();
    }
    return self::EXIT_FAILURE;
  }

  /**
   * Displays service queue item using syndication-id.
   *
   * @return int
   *   Returns 0 if operation is successful else non-zero positive integer.
   *
   * @throws \Drupal\acquia_contenthub\Exception\ContentHubClientException
   */
  private function describeServiceQueueItem(): int {
    $client = $this->getClient();
    try {
      $id = $this->input()->getOption('syndication-id');
      $response = $client->getQueueItemById($id);
      if (!$this->isResponseSuccessful($response)) {
        // Already logged in ContentHubClient.
        return self::EXIT_FAILURE;
      }

      $should_add_cdf = $this->input()->getOption('add-cdf-details');
      /** @var array $queue_item */
      $queue_item = $response['data'];
      if (!$should_add_cdf) {
        $this->renderQueueItemsResults([
          'total' => 1,
          'items' => [$queue_item],
        ]);
        return self::EXIT_SUCCESS;
      }

      $uuid = $response['data']['entity_uuid'];
      $cdf = $client->getEntity($uuid);
      $this->io()->writeln(Json::encode([
        'queue_item' => $queue_item,
        'cdf' => $cdf->toArray(),
      ]));

      return self::EXIT_SUCCESS;
    }
    catch (\Exception $e) {
      $this->io()->error(dt('Error fetching queue item: @message', ['@message' => $e->getMessage()]));
      return self::EXIT_FAILURE;
    }
  }

  /**
   * Fetches items from Service Queue and displays them in appropriate format.
   *
   * @return int
   *   Returns 0 if list is displayed successfully else 1.
   *
   * @throws \Drupal\acquia_contenthub\Exception\ContentHubClientException
   */
  private function listItemsFromServiceQueue(): int {
    $ids = $this->input()->getOption('syndication-ids');
    if (empty($ids)) {
      $response = $this->getItemsFromServiceQueue();
    }
    else {
      $response = $this->getItemsFromServiceQueueByIds(explode(',', $ids));
    }

    if (empty($response['items'])) {
      return self::EXIT_FAILURE;
    }

    $this->renderQueueItemsResults($response);
    return self::EXIT_SUCCESS;
  }

  /**
   * Fetches a paginated subset of items from the Service Queue.
   *
   * @return array{total: int, items: array<array>}
   *   Returns an array of items for the specified page (based on pagination
   *   parameters).
   *
   * @throws \Drupal\acquia_contenthub\Exception\ContentHubClientException
   */
  private function getItemsFromServiceQueue(): array {
    $params = [];
    $params['state'] = $this->input()->getOption('state');
    $params['from'] = $this->input()->getOption('from');
    $params['size'] = $this->input()->getOption('size');
    $params['client_uuid'] = $this->input()->getOption('client-uuids');
    $params['entity_uuid'] = $this->input()->getOption('entity-uuids');
    $client = $this->getClient();
    try {
      $response = $client->getAllQueueItems($params);
      if ($this->isResponseSuccessful($response) && empty($response['data'])) {
        $this->io()->note(dt('No items found in the service queue.'));
      }
    }
    catch (\Exception $e) {
      $this->io()->error(dt('Error fetching queue items: @message', ['@message' => $e->getMessage()]));
      return [];
    }

    return [
      'total' => $response['total'],
      'items' => $response['data'],
    ];
  }

  /**
   * Fetches items from Service Queue using ids.
   *
   * @return array{total: int, items: array<array>}
   *   Returns array of items.
   *
   * @throws \Drupal\acquia_contenthub\Exception\ContentHubClientException
   */
  private function getItemsFromServiceQueueByIds(array $ids): array {
    $client = $this->getClient();
    $list = [];
    foreach ($ids as $id) {
      try {
        $response = $client->getQueueItemById($id);
        if ($this->isResponseSuccessful($response)) {
          $list[] = $response['data'];
        }
      }
      catch (\Exception $e) {
        $this->io()->error(dt('Error fetching queue items: @message', ['@message' => $e->getMessage()]));
      }
    }
    return [
      'total' => count($list),
      'items' => $list,
    ];
  }

  /**
   * Displays data in tabular format or json based on --json option.
   *
   * @param array{total: int, items: array<array>} $data
   *   Array of items to be displayed.
   */
  private function renderQueueItemsResults(array $data): void {
    $json_output = $this->input()->getOption('json');
    if ($json_output) {
      $this->io()->writeln(Json::encode($data));
      return;
    }

    $content = [];
    foreach ($data['items'] as $item) {
      $content[] = [
        $item['id'] ?? '',
        $item['entity_uuid'] ?? '',
        $item['client_uuid'] ?? '',
        $item['state'] ?? '',
        $item['payload']['action'] ?? '',
        $item['payload']['type'] ?? '',
        $item['payload']['reason'] ?? '',
        $item['payload']['initiator'] ?? '',
        !empty($item['visible_at']) ? date('Y-m-d H:i:s', $item['visible_at']) : '',
        !empty($item['created_at']) ? date('Y-m-d H:i:s', $item['created_at']) : '',
        !empty($item['updated_at']) ? date('Y-m-d H:i:s', $item['updated_at']) : '',
      ];
    }

    $table = new Table($this->output());
    $table
      ->setHeaders([
        'ID',
        'Entity UUID',
        'Client UUID',
        'State',
        'Action',
        'Type',
        'Reason',
        'Initiator',
        'Visible At',
        'Created At',
        'Updated At',
      ])
      ->setRows($content)
      ->setHeaderTitle(sprintf('Found %s items in the service queue', $data['total']));
    $table->render();
  }

  /**
   * Updates an item in Service Queue.
   *
   * @return int
   *   Returns 0 if update is successful else 1.
   *
   * @throws \Drupal\acquia_contenthub\Exception\ContentHubClientException
   */
  private function updateItemInServiceQueue(): int {
    $queue_id = $this->input()->getOption('syndication-id');
    $visibility_timeout = $this->input()->getOption('visibility-timeout');
    $state = $this->input()->getOption('state');
    $reason = $this->input()->getOption('reason');

    $data = [];

    if (!empty($visibility_timeout)) {
      $data['visibility_timeout'] = $visibility_timeout;
    }

    if (!empty($state)) {
      $data['state'] = $state;
    }

    if (!empty($reason)) {
      $data['payload']['reason'] = $reason;
    }

    $client = $this->getClient();
    try {
      $response = $client->updateQueueItem($queue_id, $data);
      if (!$this->isResponseSuccessful($response)) {
        return self::EXIT_FAILURE;
      }

      $this->io()->success(dt('Queue item updated successfully.'));
      return self::EXIT_SUCCESS;
    }
    catch (\Exception $e) {
      $this->io()->error(dt('Error updating queue item: @message', ['@message' => $e->getMessage()]));
      return self::EXIT_FAILURE;
    }
  }

  /**
   * Takes confirmation and then deletes items from Service Queue.
   *
   * @return int
   *   Returns 0 if deletion is successful else 1.
   *
   * @throws \Exception
   */
  private function deleteItemsFromServiceQueue(): int {
    $all = $this->input()->getOption('all');
    $ids = $this->input()->getOption('syndication-ids');
    $uuids = $this->input()->getOption('entity-uuids');

    if (empty($all) && empty($ids) && empty($uuids)) {
      $this->io()->error(dt('Provide at least one of --all, --syndication-ids, or --entity-uuids.'));
      return static::EXIT_FAILURE;
    }

    $confirm_msg = 'Are you sure you want to proceed?';
    if ($all) {
      $confirm_msg = 'This operation will purge the entire Service Queue. ' . $confirm_msg;
    }
    else {
      $confirm_msg = 'This operation will permanently remove items from the Service Queue. ' . $confirm_msg;
    }

    if (!$this->io()->confirm(dt($confirm_msg))) {
      $this->io()->note(dt('Operation cancelled'));
      return self::EXIT_FAILURE;
    }

    if ($all) {
      return $this->removeAllQueueItems();
    }

    $client = $this->clientFactory->getClient();
    if ($ids) {
      $response = $client->deleteQueueItemsBySyndicationIds(array_map('intval', explode(',', $ids)));
      if (!$this->isResponseSuccessful($response)) {
        return self::EXIT_FAILURE;
      }
    }

    if ($uuids) {
      $response = $client->deleteQueueItemsByEntityUuids(explode(',', $uuids));
      if (!$this->isResponseSuccessful($response)) {
        return self::EXIT_FAILURE;
      }
    }

    $this->io()->success(dt('Queue items were successfully deleted.'));
    return self::EXIT_SUCCESS;
  }

  /**
   * Deletes all items from Service queue.
   *
   * @return int
   *   Returns 0 if deletion is successful else 1.
   *
   * @throws \Exception
   */
  private function removeAllQueueItems(): int {
    $client = $this->clientFactory->getClient();
    $response = $client->purgeQueue();
    if (!$this->isResponseSuccessful($response)) {
      return self::EXIT_FAILURE;
    }
    $this->io()->success(dt('Entities deleted from service queue.'));
    return self::EXIT_SUCCESS;
  }

  /**
   * Check if there is error in response.
   *
   * @return bool
   *   Returns true
   */
  private function isResponseSuccessful(?array $response): bool {
    if (is_null($response)) {
      $this->io()->error(dt('Empty response.'));
      return FALSE;
    }

    if (!isset($response['success']) || $response['success'] === FALSE) {
      $this->io()->error($response['error']['message'] ?? dt('Unknown error occurred'));
      return FALSE;
    }

    return TRUE;
  }

}
