<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Service;

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\flowdrop_interrupt\Attribute\FlowDropInterruptHandler;
use Drupal\flowdrop_interrupt\Plugin\InterruptHandler\HitlInterruptHandlerInterface;
use Drupal\flowdrop_interrupt\Plugin\InterruptHandler\InterruptHandlerInterface;
use Drupal\flowdrop_interrupt\Plugin\InterruptHandler\ScheduledInterruptHandlerInterface;

/**
 * Plugin manager for FlowDrop interrupt handlers.
 *
 * Discovers plugins under `Plugin/InterruptHandler/` in any installed module
 * via the {@see FlowDropInterruptHandler} attribute. Plugin IDs share the
 * same string space as the {@see \Drupal\flowdrop_interrupt\Enum\InterruptType}
 * enum values.
 *
 * Legacy HITL type IDs (`confirmation`, `choice`, `text`, `form`) are routed
 * to the universal `schema_form` handler — they no longer have dedicated
 * plugin classes. The mapping is in {@see self::LEGACY_HITL_TYPE_ROUTING}.
 *
 * Handler instances are cached per-request to avoid N+1 instantiation on
 * pages that iterate many interrupts (e.g. the inbox).
 *
 * Contrib plugins SHOULD NOT register IDs matching built-in
 * {@see \Drupal\flowdrop_interrupt\Enum\InterruptType} values such as
 * `confirmation`, `choice`, `text`, `form`, `schema_form`, `workflow_execution`
 * — doing so shadows the built-in handler and the legacy routing in
 * {@see self::loadHandler()} will not apply. Use namespaced IDs like
 * `my_module.slack_approval` instead.
 *
 * @api
 */
class InterruptHandlerPluginManager extends DefaultPluginManager {

  /**
   * Legacy type IDs that should resolve to the universal schema_form handler.
   *
   * @var array<string, string>
   */
  private const LEGACY_HITL_TYPE_ROUTING = [
    'confirmation' => 'schema_form',
    'choice' => 'schema_form',
    'text' => 'schema_form',
    'form' => 'schema_form',
  ];

  /**
   * Per-request handler instance cache, keyed by the requested type ID.
   *
   * @var array<string, \Drupal\flowdrop_interrupt\Plugin\InterruptHandler\InterruptHandlerInterface|null>
   */
  private array $instanceCache = [];

  /**
   * Memoised HITL type-ID list for {@see self::getHitlTypeIds()}.
   *
   * @var list<string>|null
   */
  private ?array $hitlTypeIds = NULL;

  /**
   * Memoised scheduled type-ID list for {@see self::getScheduledTypeIds()}.
   *
   * @var list<string>|null
   */
  private ?array $scheduledTypeIds = NULL;

  public function __construct(
    \Traversable $namespaces,
    CacheBackendInterface $cache_backend,
    ModuleHandlerInterface $module_handler,
  ) {
    parent::__construct(
      'Plugin/InterruptHandler',
      $namespaces,
      $module_handler,
      InterruptHandlerInterface::class,
      FlowDropInterruptHandler::class,
    );
    $this->setCacheBackend($cache_backend, 'flowdrop_interrupt_handler_plugins');
  }

  /**
   * Returns the handler for a given interrupt type ID, or NULL when none.
   *
   * Routes legacy HITL type IDs to the universal `schema_form` handler.
   * Cached per-request — calling for the same type ID multiple times in one
   * request reuses the same plugin instance.
   */
  public function loadHandler(string $typeId): ?InterruptHandlerInterface {
    if (array_key_exists($typeId, $this->instanceCache)) {
      return $this->instanceCache[$typeId];
    }

    $effectiveId = self::LEGACY_HITL_TYPE_ROUTING[$typeId] ?? $typeId;

    if (!$this->hasDefinition($effectiveId)) {
      return $this->instanceCache[$typeId] = NULL;
    }
    try {
      $instance = $this->createInstance($effectiveId);
    }
    catch (PluginException) {
      return $this->instanceCache[$typeId] = NULL;
    }

    return $this->instanceCache[$typeId] = $instance instanceof InterruptHandlerInterface ? $instance : NULL;
  }

  /**
   * Returns every interrupt type ID that resolves to a HITL handler.
   *
   * Used by the inbox controller to filter the entity query so pagers
   * operate on visible-row counts. Includes both registered plugins
   * implementing {@see HitlInterruptHandlerInterface} and the legacy type
   * IDs that route to one via {@see self::LEGACY_HITL_TYPE_ROUTING}.
   *
   * Memoised per-request — invalidated alongside the definition cache.
   *
   * @return list<string>
   *   List of HITL type IDs.
   */
  public function getHitlTypeIds(): array {
    if ($this->hitlTypeIds !== NULL) {
      return $this->hitlTypeIds;
    }
    $hitl = [];
    foreach (array_keys($this->getDefinitions()) as $pluginId) {
      $handler = $this->loadHandler($pluginId);
      if ($handler instanceof HitlInterruptHandlerInterface) {
        $hitl[$pluginId] = TRUE;
      }
    }
    foreach (self::LEGACY_HITL_TYPE_ROUTING as $legacyId => $routedId) {
      $routed = $this->loadHandler($routedId);
      if ($routed instanceof HitlInterruptHandlerInterface) {
        $hitl[$legacyId] = TRUE;
      }
    }
    return $this->hitlTypeIds = array_keys($hitl);
  }

  /**
   * Returns every interrupt type ID whose handler is schedule-driven.
   *
   * Used by the cron sweep to scope its entity query to interrupt types
   * that can resolve automatically (handlers implementing
   * {@see ScheduledInterruptHandlerInterface}).
   *
   * Memoised per-request — invalidated alongside the definition cache.
   *
   * @return list<string>
   *   List of scheduled type IDs.
   */
  public function getScheduledTypeIds(): array {
    if ($this->scheduledTypeIds !== NULL) {
      return $this->scheduledTypeIds;
    }
    $scheduled = [];
    foreach (array_keys($this->getDefinitions()) as $pluginId) {
      if ($this->loadHandler($pluginId) instanceof ScheduledInterruptHandlerInterface) {
        $scheduled[] = $pluginId;
      }
    }
    return $this->scheduledTypeIds = $scheduled;
  }

  /**
   * {@inheritdoc}
   *
   * Clear the per-request caches when plugin definitions are rebuilt — the
   * set of HITL handlers and their instances may differ afterwards.
   */
  public function clearCachedDefinitions(): void {
    parent::clearCachedDefinitions();
    $this->instanceCache = [];
    $this->hitlTypeIds = NULL;
    $this->scheduledTypeIds = NULL;
  }

}
