<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Orchestrator\Trait;

use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;
use Drupal\flowdrop_runtime\Event\PipelineCreatedEvent;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Provides common pipeline creation logic for orchestrators.
 *
 * This trait extracts the createPipelineFromWorkflow() method that is
 * nearly identical across multiple orchestrators. It uses the storage
 * interface abstraction to allow different storage backends.
 *
 * Requirements for using this trait:
 * - The class must implement getPipelineStorage() returning
 *   PipelineStorageInterface
 * - The class must implement getPipelineCreationLogger() returning
 *   LoggerInterface
 * - The class must implement getPipelineCreationEventDispatcher()
 *   returning EventDispatcherInterface
 *
 * Hook methods for customization:
 * - getPipelineLabelSuffix(): Override to customize pipeline label suffix
 * - buildPipelineExecutionContext(): Override to add custom execution context
 *
 * @see \Drupal\flowdrop_runtime\Service\Orchestrator\InterruptHandlingTrait
 *
 * @internal This trait is not part of the public API and may change without notice.
 */
trait PipelineCreationTrait {

  /**
   * Creates a pipeline from a workflow definition.
   *
   * @param array $workflow
   *   The workflow definition array with keys:
   *   - id: (string) The workflow ID
   *   - label: (string) Optional human-readable label
   *   - nodes: (array) The workflow nodes
   *   - edges: (array) The workflow edges.
   * @param array $initialData
   *   The initial input data for the pipeline.
   * @param array $options
   *   Configuration options including:
   *   - max_concurrent_jobs: (int) Maximum concurrent jobs
   *   - job_priority_strategy: (string) Priority strategy
   *   - retry_strategy: (string) Retry strategy
   *   - playground: (array) Optional playground context with session_id.
   *   - pipeline_entity_id: (int|string) Optional. If provided, the trait
   *     reuses this pre-existing pipeline entity rather than creating a new
   *     one. The caller is responsible for ensuring the entity exists in
   *     the correct storage backend. Callers that pre-create a pipeline
   *     entity (e.g. to assign the ID before orchestration starts) MUST
   *     pass this option so the orchestrator does not create a second
   *     entity. Third-party orchestrators that do not use this trait will
   *     silently ignore this option.
   *
   * @return \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface|null
   *   The created pipeline, or NULL if creation failed.
   */
  protected function createPipelineFromWorkflow(
    array $workflow,
    array $initialData,
    array $options,
  ): ?FlowDropPipelineRuntimeInterface {
    try {
      // Reuse a pre-created pipeline entity if the caller provisioned one.
      if (!empty($options['pipeline_entity_id'])) {
        $existing = $this->getPipelineStorage()->load($options['pipeline_entity_id']);
        if ($existing === NULL) {
          throw new \RuntimeException(sprintf(
            'Pre-created pipeline entity %s not found in storage for bundle %s.',
            $options['pipeline_entity_id'],
            $this->getPreferredPipelineBundle()
          ));
        }
        return $existing;
      }

      $workflowLabel = $workflow["label"] ?? $workflow["id"] ?? "Unknown Workflow";

      // Create the pipeline using the storage interface.
      $createValues = [
        "label" => $workflowLabel . $this->getPipelineLabelSuffix(),
        "bundle" => $this->getPreferredPipelineBundle(),
        "workflow_id" => $workflow["id"],
        "input_data" => $initialData,
        "status" => PipelineStatus::PENDING,
        "max_concurrent_jobs" => $options["max_concurrent_jobs"] ?? 5,
        "job_priority_strategy" => $options["job_priority_strategy"] ?? "dependency_order",
        "retry_strategy" => $options["retry_strategy"] ?? "individual",
      ];

      // Stamp the playground session this pipeline belongs to. Enables
      // reverse-lookup of pipelines by session without walking metadata.
      $playgroundSessionId = $options["playground"]["session_id"] ?? NULL;
      if ($playgroundSessionId !== NULL && $playgroundSessionId !== '') {
        $createValues["session_id"] = $playgroundSessionId;
      }

      $pipeline = $this->getPipelineStorage()->create($createValues);

      // Set execution context (allows customization via hook method).
      $executionContext = $this->buildPipelineExecutionContext($options);
      if (!empty($executionContext)) {
        $pipeline->setExecutionContext($executionContext);
      }

      // Save the pipeline.
      $this->getPipelineStorage()->save($pipeline);

      // Dispatch pipeline created event.
      $this->getPipelineCreationEventDispatcher()->dispatch(
        new PipelineCreatedEvent(
          pipeline: $pipeline,
          workflowId: $workflow["id"],
          inputData: $initialData,
          config: $options,
        ),
        PipelineCreatedEvent::NAME
      );

      $this->getPipelineCreationLogger()->info("Pipeline @id created from workflow @workflow_id", [
        "@id" => $pipeline->id(),
        "@workflow_id" => $workflow["id"],
      ]);

      return $pipeline;
    }
    catch (\Exception $e) {
      $this->getPipelineCreationLogger()->error("Failed to create pipeline from workflow @id: @message", [
        "@id" => $workflow["id"] ?? "unknown",
        "@message" => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Gets the pipeline label suffix.
   *
   * Override this method in implementing classes to customize the suffix.
   *
   * @return string
   *   The suffix to append to the pipeline label.
   */
  protected function getPipelineLabelSuffix(): string {
    return " - Pipeline " . gmdate("Y-m-d H:i:s");
  }

  /**
   * Builds the execution context for a new pipeline.
   *
   * Override this method in implementing classes to add custom context.
   * The default implementation returns an empty array.
   *
   * @param array $options
   *   The orchestration options.
   *
   * @return array
   *   The execution context to set on the pipeline.
   */
  protected function buildPipelineExecutionContext(array $options): array {
    return [];
  }

  /**
   * Gets the pipeline storage service.
   *
   * @return \Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface
   *   The pipeline storage service.
   */
  abstract protected function getPipelineStorage(): PipelineStorageInterface;

  /**
   * Gets the logger for pipeline creation operations.
   *
   * @return \Psr\Log\LoggerInterface
   *   The logger service.
   */
  abstract protected function getPipelineCreationLogger(): LoggerInterface;

  /**
   * Gets the event dispatcher for pipeline creation events.
   *
   * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
   *   The event dispatcher service.
   */
  abstract protected function getPipelineCreationEventDispatcher(): EventDispatcherInterface;

  /**
   * Returns the preferred FlowDropPipeline bundle for this orchestrator.
   *
   * Implementing classes provide this to control which storage backend
   * (and which FlowDropPipeline bundle) is used.
   *
   * @see \Drupal\flowdrop_runtime\Service\Orchestrator\PipelineAwareOrchestratorInterface
   */
  abstract public function getPreferredPipelineBundle(): string;

}
