<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Storage;

use Drupal\flowdrop_job\Constants\JobStatus;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;

/**
 * In-memory implementation of job storage.
 *
 * This implementation stores jobs in PHP arrays for fast, ephemeral
 * storage. Data is not persisted beyond the current request.
 *
 * Ideal for:
 * - SynchronousOrchestrator (fast execution without DB overhead)
 * - Unit testing (fast, isolated tests)
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class InMemoryJobStorage implements JobStorageInterface {

  /**
   * In-memory storage for jobs.
   *
   * @var \Drupal\flowdrop_job\FlowDropJobInterface[]
   */
  protected array $jobs = [];

  /**
   * Index of jobs by pipeline ID.
   *
   * @var array<string, string[]>
   */
  protected array $pipelineIndex = [];

  /**
   * {@inheritdoc}
   */
  public function create(array $data): FlowDropJobInterface {
    $job = new InMemoryJob($data);
    return $job;
  }

  /**
   * {@inheritdoc}
   */
  public function load(string|int $id): ?FlowDropJobInterface {
    return $this->jobs[$id] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultiple(array $ids): array {
    $result = [];
    foreach ($ids as $id) {
      if (isset($this->jobs[$id])) {
        $result[$id] = $this->jobs[$id];
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function save(FlowDropJobInterface $job): void {
    $id = $job->id();
    if ($id === NULL) {
      return;
    }

    $this->jobs[$id] = $job;

    // Update pipeline index.
    $pipelineId = $this->getPipelineIdFromJob($job);
    if ($pipelineId !== NULL) {
      if (!isset($this->pipelineIndex[$pipelineId])) {
        $this->pipelineIndex[$pipelineId] = [];
      }
      if (!in_array($id, $this->pipelineIndex[$pipelineId], TRUE)) {
        $this->pipelineIndex[$pipelineId][] = $id;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function delete(FlowDropJobInterface $job): void {
    $id = $job->id();
    if ($id === NULL) {
      return;
    }

    // Remove from pipeline index.
    $pipelineId = $this->getPipelineIdFromJob($job);
    if ($pipelineId !== NULL && isset($this->pipelineIndex[$pipelineId])) {
      $this->pipelineIndex[$pipelineId] = array_filter(
        $this->pipelineIndex[$pipelineId],
        fn($jobId) => $jobId !== $id
      );
    }

    unset($this->jobs[$id]);
  }

  /**
   * {@inheritdoc}
   */
  public function getByPipeline(FlowDropPipelineRuntimeInterface $pipeline): array {
    // For InMemoryPipeline, jobs are stored directly in the pipeline object.
    if ($pipeline instanceof InMemoryPipeline) {
      return $pipeline->getJobs();
    }

    // Fallback to index lookup.
    $pipelineId = (string) $pipeline->id();
    if (!isset($this->pipelineIndex[$pipelineId])) {
      return [];
    }

    return $this->loadMultiple($this->pipelineIndex[$pipelineId]);
  }

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

  /**
   * {@inheritdoc}
   */
  public function getReadyJobs(FlowDropPipelineRuntimeInterface $pipeline): array {
    $allJobs = $this->getByPipeline($pipeline);

    // Get completed job IDs.
    $completedJobIds = [];
    foreach ($allJobs as $job) {
      if ($job->getStatus() === JobStatus::COMPLETED) {
        $completedJobIds[] = $job->id();
      }
    }

    // Find ready jobs.
    $readyJobs = [];
    foreach ($allJobs as $job) {
      $status = $job->getStatus();
      if ($status === JobStatus::PENDING) {
        $readyJobs[] = $job;
      }
      elseif ($status === JobStatus::IDLE && $job->isReadyToRun($completedJobIds)) {
        $readyJobs[] = $job;
      }
    }

    return $readyJobs;
  }

  /**
   * {@inheritdoc}
   */
  public function resetCache(?array $ids = NULL): void {
    // For in-memory storage, cache reset is a no-op since there's
    // no static cache.
  }

  /**
   * Clears all stored jobs.
   *
   * Useful for testing or resetting state between workflow executions.
   */
  public function clear(): void {
    $this->jobs = [];
    $this->pipelineIndex = [];
  }

  /**
   * Gets all stored jobs.
   *
   * @return \Drupal\flowdrop_job\FlowDropJobInterface[]
   *   All stored jobs.
   */
  public function getAll(): array {
    return $this->jobs;
  }

  /**
   * Extracts pipeline ID from a job.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The job.
   *
   * @return string|null
   *   The pipeline ID, or NULL if not available.
   */
  protected function getPipelineIdFromJob(FlowDropJobInterface $job): ?string {
    // InMemoryJob stores pipeline_id in metadata or as a property.
    if ($job instanceof InMemoryJob) {
      $metadata = $job->getMetadata();
      return $metadata['pipeline_id'] ?? NULL;
    }

    // For Drupal entities, try to get from the pipeline reference.
    if ($job->hasField('pipeline')) {
      try {
        $pipelineRef = $job->get('pipeline')->target_id;
        return $pipelineRef ? (string) $pipelineRef : NULL;
      }
      catch (\Exception) {
        return NULL;
      }
    }

    return NULL;
  }

}
