<?php

/**
 * @file
 * Post-update hooks for the flowdrop_interrupt module.
 */

declare(strict_types=1);

use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptDirection;
use Drupal\flowdrop_job\FlowDropJobInterface;

/**
 * Backfill direction and job_id on existing interrupts.
 *
 * Direction is set authoritatively to Outward — every interrupt that
 * existed before the 1.13 schema additions was raised by a node and is
 * outward by definition. Inward signals (Cancel / Pause) are introduced in
 * a later phase.
 *
 * job_id is best-effort: walks each interrupt's pipeline (if any) and
 * matches against jobs whose metadata.interrupt_id equals the interrupt's
 * UUID. Rows that can't be backfilled keep job_id = NULL; the resume path
 * falls back to scanning jobs for those legacy rows.
 *
 * Batched 50 at a time to bound memory on large installations.
 *
 * @param-out array $sandbox
 */
function flowdrop_interrupt_post_update_backfill_direction_and_job_id(?array &$sandbox = NULL): void {
  $storage = \Drupal::entityTypeManager()->getStorage('flowdrop_interrupt');

  if (!isset($sandbox['ids'])) {
    $sandbox['ids'] = $storage->getQuery()->accessCheck(FALSE)->execute();
    $sandbox['total'] = count($sandbox['ids']);
    $sandbox['progress'] = 0;
    $sandbox['backfilled_jobs'] = 0;
  }

  $batch = array_splice($sandbox['ids'], 0, 50);
  foreach ($batch as $id) {
    $entity = $storage->load($id);
    if (!$entity instanceof FlowDropInterruptInterface) {
      continue;
    }

    $entity->setDirection(InterruptDirection::Outward);

    if ($entity->getJobId() === NULL) {
      $job_id = _flowdrop_interrupt_find_job_for_interrupt($entity);
      if ($job_id !== NULL) {
        $entity->setJobId($job_id);
        $sandbox['backfilled_jobs']++;
      }
    }

    $entity->save();
    $sandbox['progress']++;
  }

  $sandbox['#finished'] = $sandbox['total'] === 0
    ? 1
    : ($sandbox['progress'] / $sandbox['total']);

  if ($sandbox['#finished'] >= 1) {
    \Drupal::messenger()->addStatus(t('Backfilled @total interrupts; @jobs linked to a job.', [
      '@total' => $sandbox['progress'],
      '@jobs' => $sandbox['backfilled_jobs'],
    ]));
  }
}

/**
 * Backfill `linked_interrupt_id` from `context['linked_interrupt_id']`.
 *
 * Phase 1 stored the cascade-target UUID in the context array as
 * scaffolding. Phase 6 promotes it to a typed column. This pass migrates
 * existing rows so cascade resolution finds linked followers via the new
 * indexed column without needing a context-array fallback.
 *
 * Idempotent: rows whose typed column is already populated are skipped.
 * Rows without the context key are skipped too. Batched 100 at a time
 * (column update is cheaper than the direction/job_id backfill).
 *
 * @param-out array $sandbox
 */
function flowdrop_interrupt_post_update_backfill_linked_interrupt_id(?array &$sandbox = NULL): void {
  $storage = \Drupal::entityTypeManager()->getStorage('flowdrop_interrupt');

  if (!isset($sandbox['ids'])) {
    $sandbox['ids'] = $storage->getQuery()->accessCheck(FALSE)->execute();
    $sandbox['total'] = count($sandbox['ids']);
    $sandbox['progress'] = 0;
    $sandbox['backfilled'] = 0;
  }

  $batch = array_splice($sandbox['ids'], 0, 100);
  foreach ($batch as $id) {
    $entity = $storage->load($id);
    if (!$entity instanceof FlowDropInterruptInterface) {
      continue;
    }
    $sandbox['progress']++;

    if ($entity->getLinkedInterruptId() !== NULL) {
      continue;
    }
    $context = $entity->getContext();
    $linked = $context['linked_interrupt_id'] ?? NULL;
    if (!is_string($linked) || $linked === '') {
      continue;
    }
    $entity->setLinkedInterruptId($linked);
    $entity->save();
    $sandbox['backfilled']++;
  }

  $sandbox['#finished'] = $sandbox['total'] === 0
    ? 1
    : ($sandbox['progress'] / $sandbox['total']);

  if ($sandbox['#finished'] >= 1) {
    \Drupal::messenger()->addStatus(t('Backfilled @count interrupts with linked_interrupt_id from context.', [
      '@count' => $sandbox['backfilled'],
    ]));
  }
}

/**
 * Finds the job entity ID interrupted by the given interrupt, if any.
 *
 * Used only by the backfill — not on the runtime hot path.
 */
function _flowdrop_interrupt_find_job_for_interrupt(FlowDropInterruptInterface $interrupt): ?int {
  $pipeline_id = $interrupt->getPipelineId();
  if ($pipeline_id === NULL || $pipeline_id === '') {
    return NULL;
  }

  $pipeline_storage = \Drupal::entityTypeManager()->getStorage('flowdrop_pipeline');
  $pipeline = $pipeline_storage->load($pipeline_id);
  if ($pipeline === NULL) {
    $matches = $pipeline_storage->loadByProperties(['uuid' => $pipeline_id]);
    $pipeline = $matches ? reset($matches) : NULL;
  }
  if ($pipeline === NULL || !method_exists($pipeline, 'getJobs')) {
    return NULL;
  }

  $uuid = $interrupt->uuid();
  $node_id = $interrupt->getNodeId();
  foreach ($pipeline->getJobs() as $job) {
    if (!$job instanceof FlowDropJobInterface) {
      continue;
    }
    if ($job->getNodeId() !== $node_id) {
      continue;
    }
    $metadata = $job->getMetadata();
    if (($metadata['interrupt_id'] ?? NULL) === $uuid) {
      $id = $job->id();
      return $id !== NULL ? (int) $id : NULL;
    }
  }
  return NULL;
}
