<?php

namespace Drupal\eb\Service;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\eb\Entity\EbLogInterface;
use Drupal\eb\Entity\EbRollback;
use Drupal\eb\Entity\EbRollbackInterface;
use Drupal\eb\Entity\EbRollbackOperation;
use Drupal\eb\Entity\EbRollbackOperationInterface;
use Drupal\eb\Exception\RollbackException;
use Drupal\eb\PluginInterfaces\OperationInterface;
use Drupal\eb\PluginInterfaces\ReversibleOperationInterface;
use Drupal\eb\PluginManager\EbOperationPluginManager;
use Drupal\eb\Result\ExecutionResult;
use Drupal\eb\Result\RollbackResult;

/**
 * Service for managing operation rollbacks using ContentEntity storage.
 *
 * Rollbacks are stored as EbRollback and EbRollbackOperation
 * ContentEntity types in the database, not exported with config.
 */
class RollbackManager implements RollbackManagerInterface {

  /**
   * The current active rollback entity during a definition apply.
   *
   * @var \Drupal\eb\Entity\EbRollbackInterface|null
   */
  protected ?EbRollbackInterface $currentRollback = NULL;

  /**
   * The current operation sequence counter.
   *
   * @var int
   */
  protected int $operationSequence = 0;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   Time service.
   * @param \Drupal\Core\Session\AccountInterface $currentUser
   *   Current user.
   * @param \Drupal\eb\PluginManager\EbOperationPluginManager $operationManager
   *   Operation plugin manager.
   * @param \Drupal\eb\Service\EbLogManagerInterface $ebLogManager
   *   The EbLog manager service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory service.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected TimeInterface $time,
    protected AccountInterface $currentUser,
    protected EbOperationPluginManager $operationManager,
    protected EbLogManagerInterface $ebLogManager,
    protected ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function startRollback(string $definitionId, string $label): EbRollbackInterface {
    $rollback = EbRollback::create([
      'definition_id' => $definitionId,
      'label' => $label,
      'status' => 'pending',
      'uid' => $this->currentUser->id(),
    ]);
    $rollback->save();

    $this->currentRollback = $rollback;
    $this->operationSequence = 0;

    return $rollback;
  }

  /**
   * {@inheritdoc}
   */
  public function finalizeRollback(): void {
    if ($this->currentRollback !== NULL) {
      $this->currentRollback->setOperationCount($this->operationSequence);
      $this->currentRollback->save();
      $this->currentRollback = NULL;
      $this->operationSequence = 0;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function storeRollbackData(
    OperationInterface $operation,
    ExecutionResult $result,
    ?string $definitionId = NULL,
  ): EbRollbackOperationInterface {
    $rollbackData = $result->getRollbackData();

    if (empty($rollbackData)) {
      throw new \InvalidArgumentException('No rollback data provided in execution result');
    }

    // Get operation plugin definition.
    $pluginDefinition = $operation->getPluginDefinition();
    $operationType = $pluginDefinition['id'] ?? 'unknown';
    $description = (string) ($pluginDefinition['label'] ?? 'Unknown Operation');

    // Use current rollback or create ad-hoc rollback.
    $rollback = $this->currentRollback;
    if ($rollback === NULL) {
      $rollback = $this->startRollback($definitionId ?? 'adhoc', 'Single operation');
    }

    // Create operation entity.
    $opEntity = EbRollbackOperation::create([
      'rollback_id' => $rollback->id(),
      'definition_id' => $rollback->getDefinitionId(),
      'operation_type' => $operationType,
      'description' => $description,
      'original_data' => $rollbackData,
      'sequence' => $this->operationSequence++,
    ]);
    $opEntity->save();

    return $opEntity;
  }

  /**
   * {@inheritdoc}
   */
  public function executeRollback(int $rollbackId): RollbackResult {
    $rollback = $this->loadRollback($rollbackId);

    if ($rollback === NULL) {
      throw new RollbackException('Rollback entity not found', [], 0);
    }

    if ($rollback->isCompleted()) {
      throw new RollbackException('Rollback already executed', [], 0);
    }

    $definitionId = $rollback->getDefinitionId() ?? 'rollback';

    // Log rollback start in history.
    $ebLog = $this->ebLogManager->log([
      'label' => $definitionId . ' (rollback)',
      'definition_id' => $definitionId,
      'action' => 'rollback',
      'operation_count' => $rollback->getOperationCount(),
      'status' => 'pending',
      'rollback_id' => $rollbackId,
    ]);

    try {
      // Load operations and sort by sequence descending (reverse order).
      $operations = $this->getOperationsForRollback($rollbackId);
      usort($operations, fn($a, $b) => $b->getSequence() <=> $a->getSequence());

      $restoredEntities = [];
      foreach ($operations as $opEntity) {
        $opResult = $this->executeOperationRollback($opEntity);
        // Merge restored entities from sub-results.
        foreach ($opResult->getRestoredEntities() as $entity) {
          $restoredEntities[] = $entity;
        }
      }

      // Update rollback status.
      $rollback->setStatus('completed');
      $rollback->setCompletedTime($this->time->getRequestTime());
      $rollback->save();

      $this->recordHistorySuccess($ebLog);

      return new RollbackResult(TRUE, $restoredEntities);
    }
    catch (\Exception $e) {
      $rollback->setStatus('failed');
      $rollback->save();

      $this->recordHistoryFailure($ebLog, $e->getMessage());

      throw new RollbackException(
        'Rollback execution failed: ' . $e->getMessage(),
        [],
        0,
        $e
      );
    }
  }

  /**
   * Execute rollback for a single operation entity.
   *
   * @param \Drupal\eb\Entity\EbRollbackOperationInterface $opEntity
   *   The operation entity.
   *
   * @return \Drupal\eb\Result\RollbackResult
   *   The result of the operation rollback.
   *
   * @throws \Drupal\eb\Exception\RollbackException
   *   If the operation does not support rollback.
   */
  protected function executeOperationRollback(EbRollbackOperationInterface $opEntity): RollbackResult {
    $operation = $this->operationManager->createInstance($opEntity->getOperationType(), [
      'data' => [
        'rollback_data' => $opEntity->getOriginalData(),
      ],
    ]);

    // Verify operation supports rollback (ISP compliance).
    if (!$operation instanceof ReversibleOperationInterface) {
      throw new RollbackException(
        sprintf('Operation "%s" does not support rollback', $opEntity->getOperationType()),
        [],
        0
      );
    }

    return $operation->rollback();
  }

  /**
   * {@inheritdoc}
   */
  public function loadRollback(int $rollbackId): ?EbRollbackInterface {
    $entity = $this->entityTypeManager
      ->getStorage('eb_rollback')
      ->load($rollbackId);

    return $entity instanceof EbRollbackInterface ? $entity : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getOperationsForRollback(int $rollbackId): array {
    /** @var array<\Drupal\eb\Entity\EbRollbackOperationInterface> $operations */
    $operations = $this->entityTypeManager
      ->getStorage('eb_rollback_operation')
      ->loadByProperties(['rollback_id' => $rollbackId]);

    return $operations;
  }

  /**
   * {@inheritdoc}
   */
  public function listRollbacks(array $conditions = [], int $limit = 50): array {
    $query = $this->entityTypeManager
      ->getStorage('eb_rollback')
      ->getQuery()
      ->accessCheck(TRUE)
      ->sort('created', 'DESC')
      ->range(0, $limit);

    foreach ($conditions as $field => $value) {
      $query->condition($field, $value);
    }

    $ids = $query->execute();

    /** @var array<\Drupal\eb\Entity\EbRollbackInterface> $entities */
    $entities = $this->entityTypeManager
      ->getStorage('eb_rollback')
      ->loadMultiple($ids);

    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  public function listRollbacksByDefinition(string $definitionId, ?string $status = NULL): array {
    $conditions = ['definition_id' => $definitionId];
    if ($status !== NULL) {
      $conditions['status'] = $status;
    }
    return $this->listRollbacks($conditions);
  }

  /**
   * {@inheritdoc}
   */
  public function validateRollback(int $rollbackId): array {
    $rollback = $this->loadRollback($rollbackId);

    if ($rollback === NULL) {
      return [
        'valid' => FALSE,
        'errors' => ['Rollback entity not found'],
      ];
    }

    if ($rollback->isCompleted()) {
      return [
        'valid' => FALSE,
        'errors' => ['Rollback already executed'],
      ];
    }

    // Check operations exist.
    $operations = $this->getOperationsForRollback($rollbackId);
    if (empty($operations)) {
      return [
        'valid' => FALSE,
        'errors' => ['No operations found for rollback'],
      ];
    }

    return [
      'valid' => TRUE,
      'errors' => [],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function purgeOldRollbacks(?int $retentionDays = NULL): int {
    if ($retentionDays === NULL) {
      $retentionDays = $this->configFactory
        ->get('eb.settings')
        ->get('rollback_retention_days') ?? 30;
    }

    $cutoff = $this->time->getRequestTime() - ($retentionDays * 86400);

    $rollbackStorage = $this->entityTypeManager->getStorage('eb_rollback');
    $opStorage = $this->entityTypeManager->getStorage('eb_rollback_operation');

    $rollbackIds = $rollbackStorage->getQuery()
      ->accessCheck(FALSE)
      ->condition('created', $cutoff, '<')
      ->execute();

    if (empty($rollbackIds)) {
      return 0;
    }

    // Delete operations first (referential integrity).
    $opIds = $opStorage->getQuery()
      ->accessCheck(FALSE)
      ->condition('rollback_id', $rollbackIds, 'IN')
      ->execute();

    if (!empty($opIds)) {
      $opStorage->delete($opStorage->loadMultiple($opIds));
    }

    // Delete rollbacks.
    $rollbacks = $rollbackStorage->loadMultiple($rollbackIds);
    $rollbackStorage->delete($rollbacks);

    return count($rollbackIds);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteByDefinition(string $definitionId): int {
    $rollbackStorage = $this->entityTypeManager->getStorage('eb_rollback');
    $opStorage = $this->entityTypeManager->getStorage('eb_rollback_operation');

    $rollbackIds = $rollbackStorage->getQuery()
      ->accessCheck(FALSE)
      ->condition('definition_id', $definitionId)
      ->execute();

    if (empty($rollbackIds)) {
      return 0;
    }

    // Delete operations first (referential integrity).
    $opIds = $opStorage->getQuery()
      ->accessCheck(FALSE)
      ->condition('rollback_id', $rollbackIds, 'IN')
      ->execute();

    if (!empty($opIds)) {
      $opStorage->delete($opStorage->loadMultiple($opIds));
    }

    // Delete rollbacks.
    $rollbacks = $rollbackStorage->loadMultiple($rollbackIds);
    $rollbackStorage->delete($rollbacks);

    return count($rollbackIds);
  }

  /**
   * Record a successful rollback in history.
   *
   * @param \Drupal\eb\Entity\EbLogInterface $ebLog
   *   The EbLog entity.
   */
  protected function recordHistorySuccess(EbLogInterface $ebLog): void {
    $this->ebLogManager->complete($ebLog, 'success', 1, 0);
  }

  /**
   * Record a failed rollback in history.
   *
   * @param \Drupal\eb\Entity\EbLogInterface $ebLog
   *   The EbLog entity.
   * @param string $error
   *   The error message.
   */
  protected function recordHistoryFailure(EbLogInterface $ebLog, string $error): void {
    $this->ebLogManager->complete($ebLog, 'failed', 0, 1);
  }

}
