<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\flowdrop_job\Constants\JobStatus;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Drupal\flowdrop_trigger\Enum\OverlapPolicy;
use Psr\Log\LoggerInterface;

/**
 * Applies schedule overlap policies for cron-triggered workflows.
 *
 * Prevents pipeline pile-up when a cron trigger fires while a previous
 * pipeline from the same workflow is still active.
 *
 * @internal
 */
class OverlapPolicyHandler {

  private const BUFFER_STATE_PREFIX = 'flowdrop_trigger.buffer.';

  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly StateInterface $state,
    protected readonly LoggerInterface $logger,
  ) {}

  /**
   * Resolves whether a cron trigger should proceed, and with which data.
   *
   * For the Buffer policy this also handles firing deferred runs: if a
   * previously buffered run is now unblocked (its blocking pipeline has
   * reached a terminal state) the buffered trigger data is returned instead
   * of the current one, and the buffer is cleared.
   *
   * @param string $policy
   *   The OverlapPolicy value from orchestrator settings.
   * @param string $workflowId
   *   Workflow ID to check for active pipelines.
   * @param string $triggerConfigId
   *   Trigger configuration ID (used as buffer key).
   * @param array<string, mixed> $triggerData
   *   Trigger data for the current cron firing.
   * @param array<string, mixed> $context
   *   Cron context for the current firing (used when buffering).
   *
   * @return array{bool, array<string, mixed>}
   *   Two-element array: [proceed, effectiveTriggerData].
   *   When proceed is FALSE the caller should abort execution.
   *   When proceed is TRUE the caller should use effectiveTriggerData
   *   (may differ from $triggerData when a buffered run fires).
   */
  public function resolveExecution(
    string $policy,
    string $workflowId,
    string $triggerConfigId,
    array $triggerData,
    array $context,
  ): array {
    // For Buffer: check whether a previously buffered run can now fire.
    if ($policy === OverlapPolicy::Buffer->value) {
      $buffered = $this->popReadyBuffer($triggerConfigId);
      if ($buffered !== NULL) {
        $this->logger->info(
          'Overlap policy Buffer: firing buffered run for cron trigger @trigger (workflow @workflow).',
          ['@trigger' => $triggerConfigId, '@workflow' => $workflowId],
        );
        return [TRUE, $buffered['trigger_data']];
      }
    }

    $activePipelines = $this->loadActivePipelines($workflowId);
    if (empty($activePipelines)) {
      return [TRUE, $triggerData];
    }

    $overlapPolicy = OverlapPolicy::tryFrom($policy) ?? OverlapPolicy::Skip;

    $proceed = match ($overlapPolicy) {
      OverlapPolicy::Skip => $this->applySkip($triggerConfigId, $workflowId),
      OverlapPolicy::Buffer => $this->applyBuffer($triggerConfigId, $workflowId, $triggerData, $context, $activePipelines),
      OverlapPolicy::Cancel => $this->applyCancel($triggerConfigId, $workflowId, $activePipelines),
      OverlapPolicy::Terminate => $this->applyTerminate($triggerConfigId, $workflowId, $activePipelines),
    };

    return [$proceed, $triggerData];
  }

  /**
   * Pops a buffered run if its blocking pipeline has reached a terminal state.
   *
   * @param string $triggerConfigId
   *   The trigger configuration ID.
   *
   * @return array<string, mixed>|null
   *   The buffered payload array, or NULL if none exists or is not yet ready.
   */
  public function popReadyBuffer(string $triggerConfigId): ?array {
    $buffer = $this->state->get(self::BUFFER_STATE_PREFIX . $triggerConfigId);
    if (empty($buffer)) {
      return NULL;
    }

    $blockedByPipelineId = $buffer['blocked_by_pipeline'] ?? NULL;
    if ($blockedByPipelineId === NULL) {
      $this->clearBuffer($triggerConfigId);
      return $buffer;
    }

    /** @var \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface|null $pipeline */
    $pipeline = $this->entityTypeManager
      ->getStorage('flowdrop_pipeline')
      ->load($blockedByPipelineId);

    if ($pipeline === NULL || !$this->isPipelineActive($pipeline)) {
      $this->clearBuffer($triggerConfigId);
      return $buffer;
    }

    return NULL;
  }

  /**
   * Clears any buffered run for a trigger config.
   *
   * @param string $triggerConfigId
   *   The trigger configuration ID.
   */
  public function clearBuffer(string $triggerConfigId): void {
    $this->state->delete(self::BUFFER_STATE_PREFIX . $triggerConfigId);
  }

  /**
   * Loads non-terminal pipelines for a workflow.
   *
   * @param string $workflowId
   *   The workflow ID.
   *
   * @return \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface[]
   *   Active pipeline entities keyed by ID.
   */
  protected function loadActivePipelines(string $workflowId): array {
    $storage = $this->entityTypeManager->getStorage('flowdrop_pipeline');
    $ids = $storage->getQuery()
      ->condition('workflow_id', $workflowId)
      ->condition('status', [
        PipelineStatus::PENDING,
        PipelineStatus::RUNNING,
        PipelineStatus::PAUSED,
      ], 'IN')
      ->accessCheck(FALSE)
      ->execute();

    /** @var array<\Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface> $pipelines */
    $pipelines = $ids ? $storage->loadMultiple($ids) : [];
    return $pipelines;
  }

  /**
   * Returns TRUE if the pipeline is in a non-terminal state.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline entity.
   *
   * @return bool
   *   TRUE if active, FALSE if terminal.
   */
  protected function isPipelineActive(FlowDropPipelineInterface $pipeline): bool {
    return in_array($pipeline->getStatus(), [
      PipelineStatus::PENDING,
      PipelineStatus::RUNNING,
      PipelineStatus::PAUSED,
    ], TRUE);
  }

  /**
   * Skip: log and block execution.
   */
  protected function applySkip(string $triggerConfigId, string $workflowId): bool {
    $this->logger->info(
      'Overlap policy Skip: cron trigger @trigger skipped — workflow @workflow has an active pipeline.',
      ['@trigger' => $triggerConfigId, '@workflow' => $workflowId],
    );
    return FALSE;
  }

  /**
   * Buffer: store trigger data for later and block current execution.
   *
   * Only one run is buffered at a time. If a run is already buffered the
   * current firing is dropped (not double-buffered).
   *
   * @param string $triggerConfigId
   *   The trigger configuration ID.
   * @param string $workflowId
   *   The workflow ID.
   * @param array<string, mixed> $triggerData
   *   The trigger data to buffer.
   * @param array<string, mixed> $context
   *   The cron context to buffer.
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface[] $activePipelines
   *   Currently active pipelines.
   *
   * @return false
   *   Always blocks execution.
   */
  protected function applyBuffer(
    string $triggerConfigId,
    string $workflowId,
    array $triggerData,
    array $context,
    array $activePipelines,
  ): bool {
    if (!empty($this->state->get(self::BUFFER_STATE_PREFIX . $triggerConfigId))) {
      $this->logger->info(
        'Overlap policy Buffer: cron trigger @trigger skipped — a buffered run is already waiting for workflow @workflow.',
        ['@trigger' => $triggerConfigId, '@workflow' => $workflowId],
      );
      return FALSE;
    }

    $activePipeline = reset($activePipelines);
    $this->state->set(self::BUFFER_STATE_PREFIX . $triggerConfigId, [
      'trigger_data' => $triggerData,
      'context' => $context,
      'blocked_by_pipeline' => $activePipeline->id(),
      'buffered_at' => time(),
    ]);

    $this->logger->info(
      'Overlap policy Buffer: cron trigger @trigger buffered — waiting for pipeline @pipeline (workflow @workflow) to finish.',
      [
        '@trigger' => $triggerConfigId,
        '@pipeline' => $activePipeline->id(),
        '@workflow' => $workflowId,
      ],
    );
    return FALSE;
  }

  /**
   * Cancel: mark active pipelines as cancelled and proceed.
   *
   * @param string $triggerConfigId
   *   The trigger configuration ID.
   * @param string $workflowId
   *   The workflow ID.
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface[] $activePipelines
   *   Currently active pipelines.
   *
   * @return true
   *   Always proceeds after cancelling.
   */
  protected function applyCancel(
    string $triggerConfigId,
    string $workflowId,
    array $activePipelines,
  ): bool {
    foreach ($activePipelines as $pipeline) {
      $pipeline->markAsCancelled();
      $pipeline->save();
      $this->logger->info(
        'Overlap policy Cancel: pipeline @pipeline cancelled to allow cron trigger @trigger to run (workflow @workflow).',
        [
          '@pipeline' => $pipeline->id(),
          '@trigger' => $triggerConfigId,
          '@workflow' => $workflowId,
        ],
      );
    }
    return TRUE;
  }

  /**
   * Terminate: cancel active jobs and pipelines, then proceed.
   *
   * Unlike Cancel, Terminate also marks all active jobs (idle, pending,
   * running, interrupted) as cancelled so they are not picked up by queue
   * workers after the pipeline is cancelled.
   *
   * @param string $triggerConfigId
   *   The trigger configuration ID.
   * @param string $workflowId
   *   The workflow ID.
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface[] $activePipelines
   *   Currently active pipelines.
   *
   * @return true
   *   Always proceeds after terminating.
   */
  protected function applyTerminate(
    string $triggerConfigId,
    string $workflowId,
    array $activePipelines,
  ): bool {
    foreach ($activePipelines as $pipeline) {
      foreach ($pipeline->getJobs() as $job) {
        if (in_array($job->getStatus(), JobStatus::activeStatuses(), TRUE)) {
          $job->markAsCancelled();
          $job->save();
        }
      }
      $pipeline->markAsCancelled();
      $pipeline->save();
      $this->logger->info(
        'Overlap policy Terminate: pipeline @pipeline and its active jobs terminated to allow cron trigger @trigger to run (workflow @workflow).',
        [
          '@pipeline' => $pipeline->id(),
          '@trigger' => $triggerConfigId,
          '@workflow' => $workflowId,
        ],
      );
    }
    return TRUE;
  }

}
