<?php

namespace Drupal\eb\Service;

use Drupal\eb\Event\OperationEvents;
use Drupal\eb\Event\OperationPostExecuteEvent;
use Drupal\eb\Event\OperationPreExecuteEvent;
use Drupal\eb\PluginInterfaces\OperationInterface;
use Drupal\eb\Result\ExecutionResult;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Service for processing and executing operations.
 */
class OperationProcessor implements OperationProcessorInterface {

  /**
   * Constructor.
   *
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\eb\Service\RollbackManager $rollbackManager
   *   The rollback manager.
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger channel.
   */
  public function __construct(
    protected EventDispatcherInterface $eventDispatcher,
    protected RollbackManager $rollbackManager,
    protected LoggerInterface $logger,
  ) {}

  /**
   * Execute a single operation.
   *
   * This is the main execution method that:
   * 1. Dispatches pre-execute event (allows cancellation)
   * 2. Executes the operation (creates/updates/deletes entities)
   * 3. Stores rollback data if successful (for undo capability)
   * 4. Dispatches post-execute event
   * 5. Handles errors gracefully.
   *
   * @param \Drupal\eb\PluginInterfaces\OperationInterface $operation
   *   The operation to execute.
   * @param array<string, mixed> $context
   *   Optional execution context with keys:
   *   - definition_id: Source definition ID for rollback grouping.
   *
   * @return \Drupal\eb\Result\ExecutionResult
   *   The execution result with success status, messages, and rollback data.
   */
  public function executeOperation(
    OperationInterface $operation,
    array $context = [],
  ): ExecutionResult {
    try {
      // Dispatch pre-execute event to allow external modules to cancel or
      // modify the operation before execution.
      $preEvent = new OperationPreExecuteEvent($operation);
      $this->eventDispatcher->dispatch($preEvent, OperationEvents::PRE_EXECUTE);

      // Check if execution was cancelled by an event subscriber.
      if ($preEvent->isCancelled()) {
        $result = new ExecutionResult(FALSE);
        $result->addError($preEvent->getCancellationMessage() ?: 'Operation cancelled by event subscriber');
        $this->logger->info('Operation cancelled by event subscriber: @message', [
          '@message' => $preEvent->getCancellationMessage(),
        ]);
        return $result;
      }

      // Execute the operation (e.g., create a field, bundle, menu, etc.).
      // The operation returns an ExecutionResult with:
      // - Success status
      // - Created/modified/deleted entity information
      // - Rollback data (original state before changes).
      $result = $operation->execute();

      // Log operation execution for audit trail (shown in EbLog "Show" page).
      $this->logOperationExecution($operation, $result);

      // Store rollback data if execution was successful.
      // Rollback data allows operations to be undone later.
      if ($result->isSuccess() && !empty($result->getRollbackData())) {
        try {
          // Store the rollback data as a config entity.
          // This includes:
          // - Operation type
          // - Original data (entity state before changes)
          // - Execution timestamp and user
          // - Definition ID (if provided in context).
          $definitionId = $context['definition_id'] ?? NULL;
          $this->rollbackManager->storeRollbackData(
            $operation,
            $result,
            $definitionId,
          );
        }
        catch (\Exception $e) {
          // If rollback storage fails, log the error but don't fail
          // the operation. The operation succeeded; we just can't undo it.
          $this->logger->warning('Failed to store rollback data: @message', [
            '@message' => $e->getMessage(),
          ]);
        }
      }

      // Dispatch post-execute event to allow external modules to react to
      // the completed operation (logging, notifications, workflow triggers).
      $postEvent = new OperationPostExecuteEvent($operation, $result);
      $this->eventDispatcher->dispatch($postEvent, OperationEvents::POST_EXECUTE);

      return $result;
    }
    catch (\Exception $e) {
      // If operation execution throws an exception, catch it and return
      // a failed result instead of letting it propagate.
      $this->logger->error('Operation execution failed: @message', [
        '@message' => $e->getMessage(),
      ]);

      // Create a failed execution result with the error message.
      $result = new ExecutionResult(FALSE);
      $result->addError($e->getMessage());

      // Still dispatch post-execute event for failed operations so that
      // subscribers can handle failures (e.g., cleanup, notifications).
      $postEvent = new OperationPostExecuteEvent($operation, $result);
      $this->eventDispatcher->dispatch($postEvent, OperationEvents::POST_EXECUTE);

      return $result;
    }
  }

  /**
   * Execute multiple operations in sequence.
   *
   * Processes a batch of operations one by one. Useful for importing
   * multiple entity definitions from a single file.
   *
   * @param array<\Drupal\eb\PluginInterfaces\OperationInterface> $operations
   *   Array of operations to execute.
   * @param bool $stop_on_failure
   *   Whether to stop on first failure. Set to TRUE for atomic operations
   *   where all must succeed or none should be applied. Set to FALSE to
   *   continue processing even if some operations fail.
   * @param array<string, mixed> $context
   *   Optional execution context passed to each operation.
   *   - definition_id: Source definition ID for rollback grouping.
   *
   * @return array<\Drupal\eb\Result\ExecutionResult>
   *   Array of execution results, keyed by operation index.
   */
  public function executeBatch(
    array $operations,
    bool $stop_on_failure = TRUE,
    array $context = [],
  ): array {
    $results = [];
    $definitionId = $context['definition_id'] ?? NULL;

    // Start a rollback record if we have a definition ID.
    // This groups all operations under a single rollback entity.
    if ($definitionId !== NULL && !empty($operations)) {
      $this->rollbackManager->startRollback($definitionId, 'Apply: ' . $definitionId);
    }

    try {
      foreach ($operations as $index => $operation) {
        // Execute each operation individually with shared context.
        $result = $this->executeOperation($operation, $context);
        $results[$index] = $result;

        // Check if we should stop on failure.
        // This allows for "all or nothing" batch processing.
        if (!$result->isSuccess() && $stop_on_failure) {
          $this->logger->warning('Stopped batch execution at operation @index due to failure', [
            '@index' => $index,
          ]);
          // Stop processing remaining operations.
          break;
        }
      }
    }
    finally {
      // Always finalize the rollback, even if an exception occurred.
      // This ensures operation_count is updated and state is cleared.
      if ($definitionId !== NULL && !empty($operations)) {
        $this->rollbackManager->finalizeRollback();
      }
    }

    return $results;
  }

  /**
   * Logs operation execution details to watchdog.
   *
   * This creates detailed audit trail entries that are displayed on the
   * EbLog "Show" page. Each operation is logged with its type, description,
   * and result status.
   *
   * @param \Drupal\eb\PluginInterfaces\OperationInterface $operation
   *   The executed operation.
   * @param \Drupal\eb\Result\ExecutionResult $result
   *   The execution result.
   */
  protected function logOperationExecution(
    OperationInterface $operation,
    ExecutionResult $result,
  ): void {
    $pluginDefinition = $operation->getPluginDefinition();
    $label = (string) ($pluginDefinition['label'] ?? 'Operation');
    $operationData = $operation->getData();

    // Get operation-specific details from data.
    $details = $this->formatOperationDetails($operationData);

    // Include any messages from the result, stripped of HTML.
    $messages = $result->getMessages();
    if ($messages) {
      // Strip HTML tags and decode entities for clean log messages.
      $cleanMessages = array_map(function ($message) {
        return html_entity_decode(strip_tags($message), ENT_QUOTES, 'UTF-8');
      }, $messages);
      $messagesSuffix = ' (' . implode('; ', $cleanMessages) . ')';
    }
    else {
      $messagesSuffix = '';
    }

    // Context array includes operation data for detailed inspection.
    // The @data placeholder stores JSON-encoded operation configuration.
    $context = [
      '@label' => $label,
      '@details' => $details,
      '@messages' => $messagesSuffix,
      '@data' => json_encode($operationData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
    ];

    if ($result->isSuccess()) {
      $this->logger->info('@label: @details@messages', $context);
    }
    else {
      $errors = implode('; ', $result->getErrors());
      $context['@errors'] = $errors;
      $this->logger->error('@label FAILED: @details - @errors', $context);
    }
  }

  /**
   * Formats operation details for logging.
   *
   * Builds a human-readable description from operation data by extracting
   * common keys. This works generically for all operation types without
   * hardcoding specific operation IDs.
   *
   * @param array<string, mixed> $data
   *   The operation data.
   *
   * @return string
   *   Human-readable operation details.
   */
  protected function formatOperationDetails(array $data): string {
    $parts = [];

    // Primary identifiers in order of importance. bundle_id is used by
    // CreateBundleOperation, bundle by field operations.
    $primaryKeys = ['entity_type', 'bundle', 'bundle_id', 'field_name', 'group_name', 'name', 'id'];
    foreach ($primaryKeys as $key) {
      if (!empty($data[$key])) {
        $parts[] = $data[$key];
      }
    }

    // Add type information if available.
    $typeKeys = ['type', 'widget_type', 'formatter_type', 'format_type'];
    foreach ($typeKeys as $key) {
      if (!empty($data[$key])) {
        $parts[] = '(' . $key . ': ' . $data[$key] . ')';
        break;
      }
    }

    // Add label if available and different from other parts.
    if (!empty($data['label']) && !in_array($data['label'], $parts, TRUE)) {
      $parts[] = '"' . $data['label'] . '"';
    }

    return $parts ? implode(' ', $parts) : 'no details';
  }

}
