<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Storage;

use Drupal\Component\Uuid\Php;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\flowdrop_job\Constants\JobStatus;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;

/**
 * In-memory implementation of FlowDropPipelineRuntimeInterface.
 *
 * This value object provides a fast, ephemeral pipeline implementation
 * that doesn't persist to the database. Used by SynchronousOrchestrator
 * for lightweight workflow execution.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class InMemoryPipeline implements FlowDropPipelineRuntimeInterface {

  /**
   * The pipeline ID.
   */
  protected string $id;

  /**
   * The pipeline bundle.
   */
  protected string $bundle = PipelineStorageRegistryInterface::BUNDLE_IN_MEMORY;

  /**
   * The pipeline label.
   */
  protected string $label = '';

  /**
   * The workflow ID.
   */
  protected string $workflowId = '';

  /**
   * The pipeline status.
   */
  protected string $status = PipelineStatus::PENDING;

  /**
   * The input data.
   */
  protected array $inputData = [];

  /**
   * The output data.
   */
  protected array $outputData = [];

  /**
   * The error message.
   */
  protected string $errorMessage = '';

  /**
   * The started timestamp.
   */
  protected int $started = 0;

  /**
   * The completed timestamp.
   */
  protected int $completed = 0;

  /**
   * The max concurrent jobs.
   */
  protected int $maxConcurrentJobs = 5;

  /**
   * The job priority strategy.
   */
  protected string $jobPriorityStrategy = 'dependency_order';

  /**
   * The retry strategy.
   */
  protected string $retryStrategy = 'individual';

  /**
   * The execution context.
   */
  protected array $executionContext = [];

  /**
   * The snapshot ID.
   */
  protected ?string $snapshotId = NULL;

  /**
   * The jobs in this pipeline.
   *
   * @var \Drupal\flowdrop_job\FlowDropJobInterface[]
   */
  protected array $jobs = [];

  /**
   * The created timestamp.
   */
  protected int $created;

  /**
   * The changed timestamp.
   */
  protected int $changed;

  /**
   * The RFC-4122 UUID for this pipeline.
   */
  protected string $entityUuid;

  /**
   * The UUID generator service.
   */
  private readonly ?UuidInterface $uuidService;

  /**
   * Constructs a new InMemoryPipeline.
   *
   * @param array $data
   *   Initial data for the pipeline.
   * @param \Drupal\Component\Uuid\UuidInterface|null $uuid
   *   Optional UUID generator service.
   */
  public function __construct(array $data = [], ?UuidInterface $uuid = NULL) {
    $this->uuidService = $uuid;
    if (empty($data['id'])) {
      throw new \InvalidArgumentException('InMemoryPipeline requires a non-empty id in $data. Always use InMemoryPipelineStorage::create() to construct instances.');
    }
    $this->id = $data['id'];
    $this->entityUuid = $data['uuid'] ?? ($uuid !== NULL ? $uuid->generate() : (new Php())->generate());
    $this->created = time();
    $this->changed = time();

    if (isset($data['bundle'])) {
      $this->bundle = $data['bundle'];
    }
    if (isset($data['label'])) {
      $this->label = $data['label'];
    }
    if (isset($data['workflow_id'])) {
      $this->workflowId = is_array($data['workflow_id'])
        ? ($data['workflow_id']['target_id'] ?? '')
        : $data['workflow_id'];
    }
    if (isset($data['status'])) {
      $this->status = $data['status'];
    }
    if (isset($data['input_data'])) {
      $this->inputData = is_string($data['input_data'])
        ? json_decode($data['input_data'], TRUE) ?? []
        : $data['input_data'];
    }
    if (isset($data['max_concurrent_jobs'])) {
      $this->maxConcurrentJobs = (int) $data['max_concurrent_jobs'];
    }
    if (isset($data['job_priority_strategy'])) {
      $this->jobPriorityStrategy = $data['job_priority_strategy'];
    }
    if (isset($data['retry_strategy'])) {
      $this->retryStrategy = $data['retry_strategy'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function id(): string|int|null {
    return $this->id;
  }

  /**
   * {@inheritdoc}
   */
  public function uuid(): ?string {
    return $this->entityUuid;
  }

  /**
   * Returns the human-readable label for this pipeline.
   */
  public function label(): ?string {
    return $this->label;
  }

  /**
   * Returns the bundle (type) of this pipeline.
   */
  public function bundle(): string {
    return $this->bundle;
  }

  /**
   * Returns FALSE — in-memory pipelines are never "new" after construction.
   */
  public function isNew(): bool {
    return FALSE;
  }

  /**
   * No-op — in-memory pipelines do not track the new-entity flag.
   */
  public function enforceIsNew(bool $value = TRUE): static {
    return $this;
  }

  /**
   * Returns the last-changed timestamp.
   */
  public function changed(): int {
    return $this->changed;
  }

  /**
   * {@inheritdoc}
   */
  public function getWorkflowId(): string {
    return $this->workflowId;
  }

  /**
   * {@inheritdoc}
   */
  public function setWorkflowId(string $workflow_id): static {
    $this->workflowId = $workflow_id;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatus(): string {
    return $this->status;
  }

  /**
   * {@inheritdoc}
   */
  public function setStatus(string $status): static {
    $this->status = $status;
    $this->changed = time();
    return $this;
  }

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

  /**
   * {@inheritdoc}
   */
  public function setInputData(array $input_data): static {
    $this->inputData = $input_data;
    $this->changed = time();
    return $this;
  }

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

  /**
   * {@inheritdoc}
   */
  public function setOutputData(array $output_data): static {
    $this->outputData = $output_data;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getErrorMessage(): string {
    return $this->errorMessage;
  }

  /**
   * {@inheritdoc}
   */
  public function setErrorMessage(string $error_message): static {
    $this->errorMessage = $error_message;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getStarted(): int {
    return $this->started;
  }

  /**
   * {@inheritdoc}
   */
  public function setStarted(int $started): static {
    $this->started = $started;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCompleted(): int {
    return $this->completed;
  }

  /**
   * {@inheritdoc}
   */
  public function setCompleted(int $completed): static {
    $this->completed = $completed;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getMaxConcurrentJobs(): int {
    return $this->maxConcurrentJobs;
  }

  /**
   * {@inheritdoc}
   */
  public function setMaxConcurrentJobs(int $max_concurrent_jobs): static {
    $this->maxConcurrentJobs = $max_concurrent_jobs;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getJobPriorityStrategy(): string {
    return $this->jobPriorityStrategy;
  }

  /**
   * {@inheritdoc}
   */
  public function setJobPriorityStrategy(string $strategy): static {
    $this->jobPriorityStrategy = $strategy;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getRetryStrategy(): string {
    return $this->retryStrategy;
  }

  /**
   * {@inheritdoc}
   */
  public function setRetryStrategy(string $strategy): static {
    $this->retryStrategy = $strategy;
    $this->changed = time();
    return $this;
  }

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

  /**
   * {@inheritdoc}
   */
  public function setExecutionContext(array $context): static {
    $this->executionContext = $context;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function calculateJobCounts(): array {
    $counts = [
      JobStatus::IDLE => 0,
      JobStatus::PENDING => 0,
      JobStatus::RUNNING => 0,
      JobStatus::COMPLETED => 0,
      JobStatus::FAILED => 0,
      JobStatus::CANCELLED => 0,
      JobStatus::SKIPPED => 0,
      JobStatus::INTERRUPTED => 0,
    ];

    foreach ($this->jobs as $job) {
      $status = $job->getStatus();
      if (isset($counts[$status])) {
        $counts[$status]++;
      }
    }

    return $counts;
  }

  /**
   * {@inheritdoc}
   */
  public function isReadyToStart(): bool {
    return $this->status === PipelineStatus::PENDING;
  }

  /**
   * {@inheritdoc}
   */
  public function isRunning(): bool {
    return $this->status === PipelineStatus::RUNNING;
  }

  /**
   * {@inheritdoc}
   */
  public function isCompleted(): bool {
    return $this->status === PipelineStatus::COMPLETED;
  }

  /**
   * {@inheritdoc}
   */
  public function isFailed(): bool {
    return $this->status === PipelineStatus::FAILED;
  }

  /**
   * {@inheritdoc}
   */
  public function isCancelled(): bool {
    return $this->status === PipelineStatus::CANCELLED;
  }

  /**
   * {@inheritdoc}
   */
  public function isPaused(): bool {
    return $this->status === PipelineStatus::PAUSED;
  }

  /**
   * {@inheritdoc}
   */
  public function markAsStarted(): static {
    $this->status = PipelineStatus::RUNNING;
    $this->started = time();
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function markAsCompleted(array $output_data = []): static {
    $this->status = PipelineStatus::COMPLETED;
    $this->outputData = $output_data;
    $this->completed = time();
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function markAsFailed(string $error_message = ''): static {
    $this->status = PipelineStatus::FAILED;
    $this->errorMessage = $error_message;
    $this->completed = time();
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function markAsCancelled(): static {
    $this->status = PipelineStatus::CANCELLED;
    $this->completed = time();
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function pause(): static {
    $this->status = PipelineStatus::PAUSED;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function resume(): static {
    $this->status = PipelineStatus::RUNNING;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getWorkflow(): ?WorkflowDefinitionInterface {
    // In-memory pipelines don't load workflow entities.
    return NULL;
  }

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

  /**
   * {@inheritdoc}
   */
  public function getJobsByStatus(string $status): array {
    return array_filter($this->jobs, fn($job) => $job->getStatus() === $status);
  }

  /**
   * {@inheritdoc}
   */
  public function hasRunningJobs(): bool {
    return !empty($this->getJobsByStatus(JobStatus::RUNNING));
  }

  /**
   * {@inheritdoc}
   */
  public function hasPendingJobs(): bool {
    return !empty($this->getJobsByStatus(JobStatus::PENDING));
  }

  /**
   * {@inheritdoc}
   */
  public function hasIdleJobs(): bool {
    return !empty($this->getJobsByStatus(JobStatus::IDLE));
  }

  /**
   * {@inheritdoc}
   */
  public function hasUnfinishedJobs(): bool {
    foreach ($this->jobs as $job) {
      $status = $job->getStatus();
      if (in_array($status, JobStatus::activeStatuses(), TRUE)) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function hasInterruptedJobs(): bool {
    return !empty($this->getJobsByStatus(JobStatus::INTERRUPTED));
  }

  /**
   * {@inheritdoc}
   */
  public function allJobsCompleted(): bool {
    $terminalStatuses = [JobStatus::COMPLETED, JobStatus::FAILED, JobStatus::CANCELLED, JobStatus::SKIPPED];
    foreach ($this->jobs as $job) {
      if (!in_array($job->getStatus(), $terminalStatuses, TRUE)) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function hasFailedJobs(): bool {
    return !empty($this->getJobsByStatus(JobStatus::FAILED));
  }

  /**
   * {@inheritdoc}
   */
  public function addJob(FlowDropJobInterface $job): static {
    $jobId = $job->id();
    if ($jobId !== NULL) {
      $this->jobs[$jobId] = $job;
    }
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function removeJob(FlowDropJobInterface $job): static {
    $jobId = $job->id();
    if ($jobId !== NULL) {
      unset($this->jobs[$jobId]);
    }
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function clearJobs(): static {
    $this->jobs = [];
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getSnapshotId(): ?string {
    return $this->snapshotId;
  }

  /**
   * {@inheritdoc}
   */
  public function setSnapshotId(?string $snapshot_id): static {
    $this->snapshotId = $snapshot_id;
    $this->changed = time();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getSnapshot(): ?object {
    // In-memory pipelines don't load snapshot entities.
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function save(): int {
    return 0;
  }

  /**
   * Returns a plain-array representation of this pipeline's data.
   *
   * Useful for debugging and serialisation.
   */
  public function toArray(): array {
    return [
      'id' => $this->id,
      'label' => $this->label,
      'workflow_id' => $this->workflowId,
      'status' => $this->status,
      'input_data' => $this->inputData,
      'output_data' => $this->outputData,
      'error_message' => $this->errorMessage,
      'started' => $this->started,
      'completed' => $this->completed,
    ];
  }

  /**
   * Creates a duplicate of this pipeline with a new ID and UUID.
   */
  public function createDuplicate(): static {
    $duplicate = clone $this;
    $uuidValue = $this->uuidService !== NULL
      ? $this->uuidService->generate()
      : (new Php())->generate();
    $duplicate->id = 'inmem_' . $uuidValue;
    $duplicate->entityUuid = $uuidValue;
    return $duplicate;
  }

  /**
   * Not supported — use InMemoryPipelineStorage::create() instead.
   *
   * @throws \BadMethodCallException
   *   Always. InMemoryPipeline requires a UUID service and a pre-assigned id,
   *   both of which InMemoryPipelineStorage::create() provides. Calling the
   *   static factory directly bypasses that injection.
   */
  public static function create(array $values = []): static {
    throw new \BadMethodCallException(
      'InMemoryPipeline::create() is not supported. Use InMemoryPipelineStorage::create() instead.'
    );
  }

}
