<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Orchestrator;

use Drupal\Core\Queue\QueueFactory;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Drupal\flowdrop_pipeline\Service\JobGenerationService;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse;
use Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot;
use Drupal\flowdrop_runtime\Exception\OrchestrationException;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface;
use Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeServiceInterface;
use Drupal\flowdrop_runtime\Service\Storage\JobStorageInterface;
use Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface;
use Drupal\flowdrop_runtime\Service\WorkflowState\WorkflowStateManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Asynchronous workflow orchestrator with queue-based execution.
 *
 * This orchestrator provides pipeline and job management through Drupal's
 * queue system, enabling background processing and scalable execution.
 * Actual job execution is delegated to SynchronousPipelineOrchestrator.
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see \Drupal\flowdrop_runtime\Service\Orchestrator\AbstractOrchestratorBase
 */
class AsynchronousOrchestrator extends AbstractOrchestratorBase {

  /**
   * Constructs a new AsynchronousOrchestrator.
   *
   * @param \Drupal\flowdrop_runtime\Service\Storage\PipelineStorageInterface $pipelineStorage
   *   The pipeline storage service.
   * @param \Drupal\flowdrop_runtime\Service\Storage\JobStorageInterface $jobStorage
   *   The job storage service.
   * @param \Drupal\flowdrop_runtime\Service\Runtime\NodeRuntimeServiceInterface $nodeRuntime
   *   The node runtime service.
   * @param \Drupal\flowdrop_pipeline\Service\JobGenerationService $jobGenerationService
   *   The job generation service.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
   *   The queue factory.
   * @param \Drupal\flowdrop_runtime\Service\Orchestrator\SynchronousPipelineOrchestrator $synchronousPipelineOrchestrator
   *   The synchronous pipeline orchestrator for job execution.
   * @param \Drupal\flowdrop_runtime\Service\WorkflowState\WorkflowStateManagerInterface|null $workflowStateManager
   *   The workflow state manager (optional).
   * @param \Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface|null $realTimeManager
   *   The real-time manager (optional).
   */
  public function __construct(
    PipelineStorageInterface $pipelineStorage,
    JobStorageInterface $jobStorage,
    NodeRuntimeServiceInterface $nodeRuntime,
    JobGenerationService $jobGenerationService,
    EventDispatcherInterface $eventDispatcher,
    LoggerInterface $logger,
    protected readonly QueueFactory $queueFactory,
    protected readonly SynchronousPipelineOrchestrator $synchronousPipelineOrchestrator,
    ?WorkflowStateManagerInterface $workflowStateManager = NULL,
    ?RealTimeManagerInterface $realTimeManager = NULL,
  ) {
    parent::__construct(
      $pipelineStorage,
      $jobStorage,
      $nodeRuntime,
      $jobGenerationService,
      $eventDispatcher,
      $logger,
      $workflowStateManager,
      $realTimeManager,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getType(): string {
    return "flowdrop_runtime:asynchronous";
  }

  /**
   * {@inheritdoc}
   */
  public function orchestrate(OrchestrationRequest $request): OrchestrationResponse {
    $execution_id = $request->getPipelineId();
    $workflow_array = $request->getWorkflow();

    $this->logger->info("Starting asynchronous orchestration", [
      "execution_id" => $execution_id,
      "workflow_id" => $request->getWorkflowId(),
      "has_preloaded_state" => $request->hasPreloadedState(),
    ]);

    try {
      // Create pipeline from workflow using trait.
      $pipeline = $this->createPipelineFromWorkflow(
        $workflow_array,
        $request->getInitialData(),
        $request->getOptions()
      );

      if (!$pipeline) {
        throw new OrchestrationException("Failed to create pipeline from workflow");
      }

      // Generate jobs for the pipeline.
      try {
        $this->jobGenerationService->generateJobs($pipeline);
        $this->logger->info("Generated jobs for pipeline @id", [
          "@id" => $pipeline->id(),
        ]);
      }
      catch (\Exception $e) {
        $this->logger->error("Failed to generate jobs for pipeline @id: @message", [
          "@id" => $pipeline->id(),
          "@message" => $e->getMessage(),
        ]);
        throw new OrchestrationException(
          "Failed to generate jobs for pipeline: " . $e->getMessage(),
          0,
          $e
        );
      }

      // Initialize jobs from snapshot if available.
      $injectedCount = 0;
      if ($request->hasPreloadedState()) {
        $injectedCount = $this->initializeJobsFromSnapshot($pipeline, $request);
        $this->logger->info("Initialized @count jobs from snapshot for pipeline @id", [
          "@count" => $injectedCount,
          "@id" => $pipeline->id(),
        ]);
      }

      // Start pipeline execution.
      $this->startPipeline($pipeline);

      // Return response with pipeline information.
      return new OrchestrationResponse(
        $execution_id,
        "queued",
        [
          "pipeline_id" => $pipeline->id(),
          "status" => $pipeline->getStatus(),
          "job_counts" => $pipeline->calculateJobCounts(),
          "injected_jobs" => $injectedCount,
        ],
        0.0,
        [
          "message" => "Pipeline queued for asynchronous execution",
          "pipeline_id" => $pipeline->id(),
          "has_preloaded_state" => $request->hasPreloadedState(),
          "injected_node_count" => $injectedCount,
        ]
      );
    }
    catch (\Exception $e) {
      $this->logger->error("Asynchronous orchestration failed", [
        "execution_id" => $execution_id,
        "error" => $e->getMessage(),
      ]);

      throw new OrchestrationException(
        "Asynchronous orchestration failed: " . $e->getMessage(),
        0,
        $e
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getCapabilities(): array {
    return [
      "synchronous_execution" => FALSE,
      "parallel_execution" => TRUE,
      "async_execution" => TRUE,
      "queue_based" => TRUE,
      "retry_support" => TRUE,
      "error_recovery" => TRUE,
      "workflow_compilation" => TRUE,
      "pipeline_management" => TRUE,
      "interrupt_support" => TRUE,
    ];
  }

  /**
   * Initializes jobs from a preloaded snapshot.
   *
   * For each completed node in the snapshot, finds the corresponding job
   * and marks it as completed with the injected output data.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline with generated jobs.
   * @param \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest $request
   *   The orchestration request containing the snapshot.
   *
   * @return int
   *   The number of jobs initialized from the snapshot.
   */
  protected function initializeJobsFromSnapshot(
    FlowDropPipelineInterface $pipeline,
    OrchestrationRequest $request,
  ): int {
    $snapshot = $request->getInitialSnapshot();
    if ($snapshot === NULL) {
      return 0;
    }

    $preloadedOutputs = $request->getPreloadedNodeOutputs();
    if (empty($preloadedOutputs)) {
      return 0;
    }

    // Build a map of node ID to job.
    $jobs = $pipeline->getJobs();
    $nodeToJobMap = [];
    foreach ($jobs as $job) {
      $nodeId = $job->getNodeId();
      if ($nodeId !== "") {
        $nodeToJobMap[$nodeId] = $job;
      }
    }

    $injectedCount = 0;

    foreach ($preloadedOutputs as $nodeId => $output) {
      if (!isset($nodeToJobMap[$nodeId])) {
        $this->logger->warning("Snapshot contains completed node @node but no job found", [
          "@node" => $nodeId,
        ]);
        continue;
      }

      $job = $nodeToJobMap[$nodeId];

      // Mark the job as completed with injected data.
      $job->markAsCompleted($output);

      // Add injected metadata to the job.
      $metadata = $job->getMetadata();
      $metadata["injected"] = TRUE;
      $metadata["injected_from_snapshot"] = $snapshot->executionId;
      $metadata["injected_at"] = time();

      $nodeState = $snapshot->getNodeState($nodeId);
      if ($nodeState !== NULL) {
        $metadata["source_execution_order"] = $nodeState->executionOrder;
      }

      $job->setMetadata($metadata);
      $this->jobStorage->save($job);

      // Dispatch event for injected job completion.
      $this->dispatchEvent("flowdrop.job.injected", $job, [
        "pipeline_id" => $pipeline->id(),
        "node_id" => $nodeId,
        "output_data" => $output,
        "injected" => TRUE,
        "source_snapshot" => $snapshot->executionId,
      ]);

      $this->logger->debug("Injected completed state for job @job (node @node)", [
        "@job" => $job->id(),
        "@node" => $nodeId,
      ]);

      $injectedCount++;
    }

    return $injectedCount;
  }

  /**
   * Start a pipeline execution.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to start.
   *
   * @return bool
   *   TRUE if the pipeline was started successfully.
   */
  public function startPipeline(FlowDropPipelineInterface $pipeline): bool {
    try {
      if (!$pipeline->isReadyToStart()) {
        $this->logger->warning("Pipeline @id is not ready to start (status: @status)", [
          "@id" => $pipeline->id(),
          "@status" => $pipeline->getStatus(),
        ]);
        return FALSE;
      }

      $pipeline->markAsStarted();
      $this->pipelineStorage->save($pipeline);

      $jobs = $pipeline->getJobs();
      if (empty($jobs)) {
        throw new \RuntimeException("Pipeline has no jobs. Please generate jobs first.");
      }

      $this->dispatchEvent("flowdrop.pipeline.started", $pipeline);

      $this->logger->info("Pipeline @id started successfully with @job_count jobs", [
        "@id" => $pipeline->id(),
        "@job_count" => count($jobs),
      ]);

      $this->queuePipelineExecution($pipeline);

      return TRUE;
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to start pipeline @id: @message", [
        "@id" => $pipeline->id(),
        "@message" => $e->getMessage(),
      ]);
      return FALSE;
    }
  }

  /**
   * Queue pipeline execution.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to queue.
   */
  protected function queuePipelineExecution(FlowDropPipelineInterface $pipeline): void {
    $queue = $this->queueFactory->get("flowdrop_runtime_pipeline_execution");
    $queue->createItem([
      "pipeline_id" => $pipeline->id(),
      "action" => "execute",
      "timestamp" => time(),
    ]);
  }

  /**
   * Execute pipeline (called from queue worker).
   *
   * Uses SynchronousPipelineOrchestrator as the execution engine with
   * configurable limits.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to execute.
   */
  public function executePipeline(FlowDropPipelineInterface $pipeline): void {
    try {
      if ($pipeline->isPaused()) {
        $pipeline->resume();
        $this->pipelineStorage->save($pipeline);
        $this->logger->info("Resuming paused pipeline @id", [
          "@id" => $pipeline->id(),
        ]);
      }

      if (!$pipeline->isRunning()) {
        $this->logger->warning("Pipeline @id is not running (status: @status)", [
          "@id" => $pipeline->id(),
          "@status" => $pipeline->getStatus(),
        ]);
        return;
      }

      // Use SynchronousPipelineOrchestrator with limits for async execution.
      $maxIterations = 50;
      $maxExecutionTime = 30.0;

      $response = $this->synchronousPipelineOrchestrator->executePipeline(
        $pipeline,
        $maxIterations,
        $maxExecutionTime
      );

      $status = $response->getStatus();

      if ($status === WorkflowSnapshot::STATUS_PAUSED) {
        $this->logger->info("Pipeline @id paused, queuing for retry", [
          "@id" => $pipeline->id(),
        ]);
        $this->queuePipelineExecution($pipeline);
      }
      elseif ($status === WorkflowSnapshot::STATUS_INTERRUPTED) {
        $metadata = $response->getMetadata();
        $this->logger->info("Pipeline @id interrupted for human-in-the-loop input. Interrupt ID: @interrupt_id", [
          "@id" => $pipeline->id(),
          "@interrupt_id" => $metadata["interrupt"]["interrupt_id"] ?? "unknown",
        ]);

        $this->dispatchEvent("flowdrop.pipeline.interrupted", $pipeline, [
          "response" => $response,
          "interrupt" => $metadata["interrupt"] ?? [],
        ]);
      }
      elseif ($status === WorkflowSnapshot::STATUS_COMPLETED) {
        $this->completePipeline($pipeline, $response->getResults());
      }
      elseif ($status === WorkflowSnapshot::STATUS_FAILED) {
        $this->logger->error("Pipeline @id failed during execution", [
          "@id" => $pipeline->id(),
        ]);
      }
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to execute pipeline @id: @message", [
        "@id" => $pipeline->id(),
        "@message" => $e->getMessage(),
      ]);
      $this->failPipeline($pipeline, "Pipeline execution failed: " . $e->getMessage());
    }
  }

  /**
   * Complete a pipeline.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to complete.
   * @param array<string, mixed>|null $results
   *   Optional results from execution.
   */
  protected function completePipeline(FlowDropPipelineInterface $pipeline, ?array $results = NULL): void {
    try {
      $output_data = $results ?? $this->aggregatePipelineOutput($pipeline);

      $pipeline->markAsCompleted($output_data);
      $this->markRemainingJobsAsSkipped($pipeline);
      $this->pipelineStorage->save($pipeline);

      $this->dispatchEvent("flowdrop.pipeline.completed", $pipeline, [
        "output_data" => $output_data,
      ]);

      $this->logger->info("Pipeline @id completed successfully", ["@id" => $pipeline->id()]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to complete pipeline @id: @message", [
        "@id" => $pipeline->id(),
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Fail a pipeline.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to fail.
   * @param string $error_message
   *   The error message.
   */
  protected function failPipeline(FlowDropPipelineInterface $pipeline, string $error_message = ""): void {
    try {
      $pipeline->markAsFailed($error_message);
      $this->markRemainingJobsAsSkipped($pipeline);
      $this->pipelineStorage->save($pipeline);

      $this->dispatchEvent("flowdrop.pipeline.failed", $pipeline, [
        "error_message" => $error_message,
      ]);

      $this->logger->error("Pipeline @id failed: @message", [
        "@id" => $pipeline->id(),
        "@message" => $error_message,
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to mark pipeline @id as failed: @message", [
        "@id" => $pipeline->id(),
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Aggregate output data from completed jobs.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to aggregate output for.
   *
   * @return array
   *   The aggregated output data.
   */
  protected function aggregatePipelineOutput(FlowDropPipelineInterface $pipeline): array {
    $output_data = [];
    $completed_jobs = $pipeline->getJobsByStatus("completed");

    foreach ($completed_jobs as $job) {
      $job_output = $job->getOutputData();
      $node_id = $job->getNodeId();

      if (!empty($job_output)) {
        $output_data[$node_id] = $job_output;
      }
    }

    return $output_data;
  }

  /**
   * Handle job completion.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The completed job.
   * @param array $output_data
   *   The job output data.
   */
  public function handleJobCompletion(FlowDropJobInterface $job, array $output_data = []): void {
    try {
      $job->markAsCompleted($output_data);

      $pipeline = $job->getPipeline();
      if ($pipeline !== NULL) {
        $allJobs = $pipeline->getJobs();
        $maxOrder = 0;
        foreach ($allJobs as $otherJob) {
          $otherMetadata = $otherJob->getMetadata();
          $otherOrder = $otherMetadata["execution_order"] ?? 0;
          if ($otherOrder > $maxOrder) {
            $maxOrder = $otherOrder;
          }
        }
        $executionOrder = $maxOrder + 1;
        $jobMetadata = $job->getMetadata();
        $jobMetadata["execution_order"] = $executionOrder;
        $job->setMetadata($jobMetadata);
      }

      $this->jobStorage->save($job);

      $this->dispatchEvent("flowdrop.job.completed", $job, [
        "output_data" => $output_data,
      ]);

      $this->logger->info("Job @id completed", [
        "@id" => $job->id(),
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to handle job completion @id: @message", [
        "@id" => $job->id(),
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Handle job failure.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The failed job.
   * @param string $error_message
   *   The error message.
   */
  public function handleJobFailure(FlowDropJobInterface $job, string $error_message = ""): void {
    try {
      $job->markAsFailed($error_message);
      $this->jobStorage->save($job);

      $this->dispatchEvent("flowdrop.job.failed", $job, [
        "error_message" => $error_message,
      ]);

      if ($job->canRetry()) {
        $this->retryJob($job);
      }

      $this->logger->error("Job @id failed: @message", [
        "@id" => $job->id(),
        "@message" => $error_message,
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to handle job failure @id: @message", [
        "@id" => $job->id(),
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Retry a failed job.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The job to retry.
   */
  protected function retryJob(FlowDropJobInterface $job): void {
    $job->incrementRetryCount()
      ->setStatus("pending");
    $this->jobStorage->save($job);

    $queue = $this->queueFactory->get("flowdrop_runtime_job_execution");
    $queue->createItem([
      "job_id" => $job->id(),
      "action" => "retry",
      "timestamp" => time(),
    ]);

    $this->logger->info("Job @id queued for retry (attempt @attempt)", [
      "@id" => $job->id(),
      "@attempt" => $job->getRetryCount(),
    ]);
  }

}
