<?php

namespace Drupal\eb\PluginBase;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\eb\Exception\ExecutionException;
use Drupal\eb\Exception\RollbackException;
use Drupal\eb\PluginInterfaces\FullOperationInterface;
use Drupal\eb\Result\ExecutionResult;
use Drupal\eb\Result\PreviewResult;
use Drupal\eb\Result\RollbackResult;
use Drupal\eb\Result\ValidationResult;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Base class for Entity Builder operation plugins.
 *
 * This base class implements FullOperationInterface, meaning all operations
 * extending this class support validation, execution, preview, and rollback.
 *
 * Provides automatic dependency injection for:
 * - $this->entityTypeManager: EntityTypeManagerInterface
 * - $this->logger: LoggerInterface (eb channel)
 * - $this->configFactory: ConfigFactoryInterface
 *
 * Provides logging helper methods that respect eb.settings.log_operations:
 * - $this->logInfo(): Logs info messages (respects setting)
 * - $this->logError(): Logs error messages (always logged)
 * - $this->shouldLog(): Checks if info logging is enabled
 *
 * @see \Drupal\eb\PluginInterfaces\FullOperationInterface
 */
abstract class OperationBase extends PluginBase implements FullOperationInterface, ContainerFactoryPluginInterface {

  use StringTranslationTrait;

  /**
   * The operation configuration.
   *
   * @var array<string, mixed>
   */
  protected array $data;

  /**
   * Constructor.
   *
   * @param array<string, mixed> $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger channel.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   */
  public function __construct(
    array $configuration,
    string $plugin_id,
    mixed $plugin_definition,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected LoggerInterface $logger,
    protected ConfigFactoryInterface $configFactory,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->data = $configuration['data'] ?? [];
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    /** @var \Psr\Log\LoggerInterface $logger */
    $logger = $container->get('logger.channel.eb');

    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
      $logger,
      $container->get('config.factory'),
    );
  }

  /**
   * {@inheritdoc}
   */
  abstract public function validate(): ValidationResult;

  /**
   * {@inheritdoc}
   */
  abstract public function preview(): PreviewResult;

  /**
   * {@inheritdoc}
   */
  abstract public function execute(): ExecutionResult;

  /**
   * {@inheritdoc}
   */
  abstract public function rollback(): RollbackResult;

  /**
   * {@inheritdoc}
   */
  public function checkAccess(AccountInterface $account): AccessResult {
    // Default implementation checks for import permission.
    // Subclasses can override for more specific access control.
    return AccessResult::allowedIfHasPermission(
      $account,
      'import entity architecture'
    );
  }

  /**
   * Gets a value from operation data.
   *
   * @param string $key
   *   The data key.
   * @param mixed $default
   *   Default value if key doesn't exist.
   *
   * @return mixed
   *   The value.
   */
  protected function getDataValue(string $key, mixed $default = NULL): mixed {
    return $this->data[$key] ?? $default;
  }

  /**
   * Gets all operation data.
   *
   * @return array<string, mixed>
   *   Operation data array.
   */
  protected function getAllData(): array {
    return $this->data;
  }

  /**
   * {@inheritdoc}
   */
  public function getData(): array {
    return $this->data;
  }

  /**
   * {@inheritdoc}
   */
  public function getOperationType(): string {
    $definition = $this->getPluginDefinition();
    return $definition['operationType'] ?? 'create';
  }

  /**
   * Checks if a required field is present.
   *
   * @param string $field
   *   Field name.
   *
   * @return bool
   *   TRUE if field exists and is not empty.
   */
  protected function hasRequiredField(string $field): bool {
    return !empty($this->data[$field]);
  }

  /**
   * Validates required fields are present.
   *
   * @param array<int, string> $requiredFields
   *   Array of required field names.
   * @param \Drupal\eb\Result\ValidationResult $result
   *   Validation result to add errors to.
   */
  protected function validateRequiredFields(array $requiredFields, ValidationResult $result): void {
    foreach ($requiredFields as $field) {
      if (!$this->hasRequiredField($field)) {
        $result->addError(
          $this->t('Required field "@field" is missing.', ['@field' => $field]),
          $field,
          'missing_required_field'
        );
      }
    }
  }

  /**
   * Generates entity edit URL.
   *
   * @param string $entityType
   *   Entity type ID.
   * @param string $entityId
   *   Entity ID.
   *
   * @return string|null
   *   Edit URL or NULL if cannot be generated.
   */
  protected function getEntityEditUrl(string $entityType, string $entityId): ?string {
    try {
      $entity = $this->entityTypeManager->getStorage($entityType)->load($entityId);
      if ($entity && $entity->hasLinkTemplate('edit-form')) {
        return $entity->toUrl('edit-form')->toString();
      }
    }
    catch (\Exception $e) {
      $this->logger->warning('Failed to generate edit URL for @type @id: @message', [
        '@type' => $entityType,
        '@id' => $entityId,
        '@message' => $e->getMessage(),
      ]);
    }
    return NULL;
  }

  /**
   * Executes a callback with standardized error handling.
   *
   * Wraps execution logic in a try/catch block that converts any exception
   * into an ExecutionException with a consistent message format.
   *
   * @param callable $callback
   *   The callback to execute. Should return an ExecutionResult.
   * @param string $actionDescription
   *   Description of the action for error messages.
   * @param array $context
   *   Additional context for error messages.
   *
   * @return \Drupal\eb\Result\ExecutionResult
   *   The result from the callback.
   *
   * @throws \Drupal\eb\Exception\ExecutionException
   *   If the callback throws any exception.
   */
  protected function executeWithErrorHandling(callable $callback, string $actionDescription, array $context = []): ExecutionResult {
    try {
      return $callback();
    }
    catch (ExecutionException $e) {
      // Re-throw ExecutionExceptions as-is to preserve original message.
      throw $e;
    }
    catch (\Exception $e) {
      $context['@message'] = $e->getMessage();
      throw new ExecutionException(
        "Failed to {$actionDescription}: {$e->getMessage()}",
        $context,
        0,
        $e
      );
    }
  }

  /**
   * Executes a rollback callback with standardized error handling.
   *
   * Wraps rollback logic in a try/catch block that converts any exception
   * into a RollbackException with a consistent message format.
   *
   * @param callable $callback
   *   The callback to execute. Should return a RollbackResult.
   * @param string $actionDescription
   *   Description of what is being rolled back (e.g., "field creation").
   *
   * @return \Drupal\eb\Result\RollbackResult
   *   The result from the callback.
   *
   * @throws \Drupal\eb\Exception\RollbackException
   *   If the callback throws any exception.
   */
  protected function rollbackWithErrorHandling(callable $callback, string $actionDescription = 'operation'): RollbackResult {
    try {
      return $callback();
    }
    catch (RollbackException $e) {
      // Re-throw RollbackExceptions as-is to preserve original message.
      throw $e;
    }
    catch (\Exception $e) {
      throw new RollbackException(
        "Failed to rollback {$actionDescription}: {$e->getMessage()}",
        [],
        0,
        $e
      );
    }
  }

  /**
   * Gets rollback data, throwing an exception if not available.
   *
   * Use this at the start of rollback() methods to ensure rollback data exists.
   *
   * @return array<string, mixed>
   *   The rollback data array.
   *
   * @throws \Drupal\eb\Exception\RollbackException
   *   If no rollback data is available.
   */
  protected function getRequiredRollbackData(): array {
    $rollback_data = $this->getDataValue('rollback_data');

    if (!$rollback_data) {
      throw new RollbackException('No rollback data available');
    }

    return $rollback_data;
  }

  /**
   * Checks if operation logging is enabled.
   *
   * @return bool
   *   TRUE if logging is enabled, FALSE otherwise.
   */
  protected function shouldLog(): bool {
    return (bool) $this->configFactory->get('eb.settings')->get('log_operations');
  }

  /**
   * Logs an info message if logging is enabled.
   *
   * Respects the eb.settings.log_operations setting. If disabled,
   * the message is silently ignored.
   *
   * @param string $message
   *   The log message with placeholders.
   * @param array<string, mixed> $context
   *   Context array with placeholder values.
   */
  protected function logInfo(string $message, array $context = []): void {
    if ($this->shouldLog()) {
      $this->logger->info($message, $context);
    }
  }

  /**
   * Logs an error message.
   *
   * Error messages are always logged regardless of the log_operations setting.
   * Errors are critical for debugging and should never be suppressed.
   *
   * @param string $message
   *   The log message with placeholders.
   * @param array<string, mixed> $context
   *   Context array with placeholder values.
   */
  protected function logError(string $message, array $context = []): void {
    $this->logger->error($message, $context);
  }

}
