<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Orchestrator;

use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse;
use Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot;
use Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface;
use Drupal\flowdrop_runtime\Exception\OrchestrationException;

/**
 * Synchronous pipeline-based workflow orchestrator.
 *
 * This orchestrator executes workflows by creating a pipeline entity,
 * generating jobs, and executing them synchronously. This approach provides
 * better job tracking, persistence, and management compared to direct workflow
 * execution.
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see \Drupal\flowdrop_runtime\Service\Orchestrator\AbstractOrchestratorBase
 */
class SynchronousPipelineOrchestrator extends AbstractOrchestratorBase implements PipelineExecutorInterface {

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

  /**
   * {@inheritdoc}
   */
  public function orchestrate(OrchestrationRequest $request): OrchestrationResponse {
    $workflowData = $request->getWorkflow();
    $executionId = $this->generateExecutionId();

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

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

      // Generate jobs for the pipeline.
      $this->jobGenerationService->generateJobs($pipeline);

      // Reload the pipeline to ensure fresh job references after generation.
      $pipeline = $this->reloadPipeline($pipeline);

      // Verify jobs are available after reload.
      $jobs = $pipeline->getJobs();
      $this->logger->info("Pipeline @pipeline_id reloaded with @job_count jobs for immediate synchronous execution", [
        "@pipeline_id" => $pipeline->id(),
        "@job_count" => count($jobs),
        "@execution_id" => $executionId,
      ]);

      // Execute the pipeline immediately (synchronous execution).
      return $this->executePipeline($pipeline);
    }
    catch (InterruptExceptionInterface $e) {
      // Use the InterruptHandlingTrait for standardized interrupt handling.
      return $this->handleInterruptException(
        $e,
        $executionId,
        $request->getWorkflowId(),
        microtime(TRUE),
        [],
        ["pipeline_id" => $pipeline->id() ?? ""],
      );
    }
    catch (\Exception $e) {
      $this->logger->error("Synchronous pipeline orchestration failed: @message", [
        "@message" => $e->getMessage(),
        "@execution_id" => $executionId,
      ]);

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

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

  /**
   * Executes a pipeline by running all ready jobs synchronously.
   *
   * This method can be called directly for pipeline execution without
   * going through the orchestrate() method.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to execute.
   * @param int|null $maxIterations
   *   Maximum number of iterations before pausing (default: 100).
   *   NULL means no limit.
   * @param float|null $maxExecutionTime
   *   Maximum execution time in seconds before pausing
   *   (default: NULL = no limit).
   *
   * @return \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse
   *   The orchestration response.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\OrchestrationException
   *   When execution fails.
   */
  public function executePipeline(
    FlowDropPipelineInterface $pipeline,
    ?int $maxIterations = 100,
    ?float $maxExecutionTime = NULL,
  ): OrchestrationResponse {
    $executionId = $this->generateExecutionId();
    $startTime = microtime(TRUE);

    $this->logger->info("Starting synchronous pipeline execution for pipeline @pipeline_id", [
      "@pipeline_id" => $pipeline->id(),
      "@execution_id" => $executionId,
    ]);

    try {
      // Ensure pipeline is ready for execution.
      if ($pipeline->isReadyToStart() || $pipeline->isPaused()) {
        $pipeline->markAsStarted();
        $this->pipelineStorage->save($pipeline);
      }
      elseif (!$pipeline->isRunning()) {
        $this->logger->warning("Pipeline @pipeline_id is not in expected state for execution (status: @status). Attempting to start anyway.", [
          "@pipeline_id" => $pipeline->id(),
          "@status" => $pipeline->getStatus(),
          "@execution_id" => $executionId,
        ]);
        $pipeline->markAsStarted();
        $this->pipelineStorage->save($pipeline);
      }

      $results = [];
      $executedJobs = [];
      $iteration = 0;

      // Keep executing ready jobs until none are left or limits reached.
      while (TRUE) {
        // Check iteration limit.
        if ($maxIterations !== NULL && $iteration >= $maxIterations) {
          $this->logger->info("Reached max iterations (@max) for pipeline @pipeline_id", [
            "@max" => $maxIterations,
            "@pipeline_id" => $pipeline->id(),
            "@execution_id" => $executionId,
          ]);
          break;
        }

        // Check execution time limit.
        if ($maxExecutionTime !== NULL) {
          $elapsedTime = microtime(TRUE) - $startTime;
          if ($elapsedTime >= $maxExecutionTime) {
            $this->logger->info("Reached max execution time (@time seconds) for pipeline @pipeline_id", [
              "@time" => round($maxExecutionTime, 2),
              "@pipeline_id" => $pipeline->id(),
              "@execution_id" => $executionId,
            ]);
            break;
          }
        }

        $readyJobs = $this->jobGenerationService->getReadyJobs($pipeline);

        if (empty($readyJobs)) {
          break;
        }

        $this->logger->info("Found @count ready jobs in iteration @iteration", [
          "@count" => count($readyJobs),
          "@iteration" => $iteration,
          "@execution_id" => $executionId,
        ]);

        // Execute all ready jobs in this iteration.
        foreach ($readyJobs as $job) {
          $jobResult = $this->executeJob($job, $pipeline, $executionId);
          $results[$job->getNodeId()] = $jobResult;
          $executedJobs[] = $job->id();
        }

        $iteration++;
      }

      // Check final pipeline status.
      $executionTime = microtime(TRUE) - $startTime;
      $hasMoreWork = !empty($this->jobGenerationService->getReadyJobs($pipeline));

      if ($pipeline->hasFailedJobs()) {
        $pipeline->markAsFailed("Some jobs failed during execution");
        $status = WorkflowSnapshot::STATUS_FAILED;
        $this->markRemainingJobsAsSkipped($pipeline);
      }
      elseif ($hasMoreWork) {
        $pipeline->pause();
        $status = WorkflowSnapshot::STATUS_PAUSED;
        $reason = [];
        if ($maxIterations !== NULL && $iteration >= $maxIterations) {
          $reason[] = "max iterations ({$maxIterations})";
        }
        if ($maxExecutionTime !== NULL && $executionTime >= $maxExecutionTime) {
          $reason[] = "max execution time (" . round($maxExecutionTime, 2) . "s)";
        }
        $this->logger->warning("Pipeline @pipeline_id paused after reaching @reason. Remaining ready jobs: @count", [
          "@pipeline_id" => $pipeline->id(),
          "@reason" => implode(" and ", $reason),
          "@count" => count($this->jobGenerationService->getReadyJobs($pipeline)),
          "@execution_id" => $executionId,
        ]);
      }
      else {
        $pipeline->markAsCompleted($results);
        $status = WorkflowSnapshot::STATUS_COMPLETED;
        $this->markRemainingJobsAsSkipped($pipeline);
      }

      // Store execution time in execution context.
      $executionTimeUs = (int) round($executionTime * 1000000);
      $context = $pipeline->getExecutionContext();
      $context["execution_time_us"] = $executionTimeUs;
      $pipeline->setExecutionContext($context);

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

      $this->logger->info("Synchronous pipeline execution completed for pipeline @pipeline_id in @time seconds", [
        "@pipeline_id" => $pipeline->id(),
        "@time" => round($executionTime, 3),
        "@status" => $status,
        "@executed_jobs" => count($executedJobs),
      ]);

      // Generate and save workflow snapshot.
      $snapshot = $this->generateSnapshotFromPipeline($pipeline, $executionId, $status);
      $this->saveAndLinkSnapshot($snapshot, $pipeline);

      return new OrchestrationResponse(
        executionId: $executionId,
        status: $status,
        results: $results,
        executionTime: $executionTime,
        metadata: [
          "pipeline_id" => $pipeline->id(),
          "executed_jobs" => $executedJobs,
          "iterations" => $iteration,
          "total_jobs" => count($pipeline->getJobs()),
          "snapshot" => $snapshot?->toArray(),
        ]
      );
    }
    catch (InterruptExceptionInterface $e) {
      // Pause the pipeline for human-in-the-loop input.
      $pipeline->pause();
      $this->pipelineStorage->save($pipeline);

      return $this->handleInterruptException(
        $e,
        $executionId,
        $pipeline->getWorkflowId(),
        $startTime,
        $results,
        [
          "pipeline_id" => $pipeline->id(),
          "executed_jobs" => $executedJobs,
          "iterations" => $iteration,
        ],
      );
    }
    catch (\Exception $e) {
      $pipeline->markAsFailed("Pipeline execution failed: " . $e->getMessage());
      $this->pipelineStorage->save($pipeline);

      $this->logger->error("Synchronous pipeline execution failed for pipeline @pipeline_id: @message", [
        "@pipeline_id" => $pipeline->id(),
        "@message" => $e->getMessage(),
        "@execution_id" => $executionId,
      ]);

      throw new OrchestrationException(
        "Synchronous pipeline execution failed: " . $e->getMessage(),
        0,
        $e
      );
    }
  }

  /**
   * Saves a snapshot and links it to the pipeline.
   *
   * @param \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot|null $snapshot
   *   The snapshot to save.
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline to link the snapshot to.
   */
  protected function saveAndLinkSnapshot($snapshot, FlowDropPipelineInterface $pipeline): void {
    if ($snapshot === NULL || $this->workflowStateManager === NULL) {
      return;
    }

    try {
      $snapshot = $this->workflowStateManager->save($snapshot);
      $this->logger->debug("Saved workflow snapshot @exec for workflow @workflow", [
        "@exec" => $snapshot->executionId,
        "@workflow" => $snapshot->workflowId,
      ]);

      // Link the snapshot to the pipeline for unified tracking.
      $snapshotEntity = $this->workflowStateManager->getSnapshotEntity($snapshot->executionId);
      if ($snapshotEntity !== NULL) {
        $pipeline->setSnapshotId((string) $snapshotEntity->id());
        $this->pipelineStorage->save($pipeline);
        $this->logger->debug("Linked snapshot @snapshot_id to pipeline @pipeline_id", [
          "@snapshot_id" => $snapshotEntity->id(),
          "@pipeline_id" => $pipeline->id(),
        ]);
      }
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to save workflow snapshot: @error", [
        "@error" => $e->getMessage(),
        "@exec" => $snapshot->executionId ?? "unknown",
      ]);
    }
  }

}
