<?php

declare(strict_types=1);

namespace Drupal\flowdrop\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\FlowDropNodeProcessorInterface;

/**
 * Plugin manager for FlowDropNode plugins using attribute-based discovery.
 *
 * This manager automatically namespaces plugin IDs with their provider module
 * to prevent ID collisions when multiple modules define plugins with the same
 * base ID. For example, a plugin with `id: "chat_model"` from module
 * `flowdrop_ai_provider` becomes `flowdrop_ai_provider:chat_model`.
 *
 * The namespacing format is: `{provider}:{original_id}`
 *
 * Supports plugin derivers via DerivativeDiscoveryDecorator, enabling modules
 * to dynamically generate multiple plugin instances from a single base plugin.
 * Derivative plugins (e.g., `tool_api:entity_save`) are also namespaced with
 * their provider to ensure uniqueness across modules.
 *
 * @see \Drupal\flowdrop\Attribute\FlowDropNodeProcessor
 *
 * @api
 */
class FlowDropNodeProcessorPluginManager extends DefaultPluginManager {

  /**
   * The separator used between provider and plugin ID.
   */
  public const NAMESPACE_SEPARATOR = ":";

  /**
   * Constructs a FlowDropNodeProcessorPluginManager object.
   *
   * @param \Traversable $namespaces
   *   An object that implements \Traversable which contains the root paths
   *   keyed by the corresponding namespace to look for plugin implementations.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Cache backend instance to use.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to invoke the alter hook.
   */
  public function __construct(
    \Traversable $namespaces,
    CacheBackendInterface $cache_backend,
    ModuleHandlerInterface $module_handler,
  ) {
    parent::__construct(
      'Plugin/FlowDropNodeProcessor',
      $namespaces,
      $module_handler,
      FlowDropNodeProcessorInterface::class,
      FlowDropNodeProcessor::class
    );
    $this->setCacheBackend($cache_backend, 'flowdrop_node_processor_plugins');
    $this->alterInfo('flowdrop_node_processor_info');
  }

  /**
   * {@inheritdoc}
   *
   * Processes plugin definitions to add provider-based namespacing.
   *
   * This method prefixes each plugin ID with its provider module name,
   * ensuring unique IDs across all contributing modules. The original
   * (non-prefixed) ID is preserved in the 'original_id' property.
   *
   * For derivative plugins (e.g., `tool_api:entity_save`), the full composite
   * ID is namespaced with the provider to ensure unique IDs across modules.
   * The derivative's ":" separator is distinct from the provider namespace.
   *
   * @param array<string, mixed> $definition
   *   The plugin definition to process.
   * @param string $plugin_id
   *   The plugin ID.
   */
  public function processDefinition(&$definition, $plugin_id): void {
    parent::processDefinition($definition, $plugin_id);

    // Store the original ID before prefixing.
    $definition["original_id"] = $plugin_id;

    // Ensure the definition has an ID set (derivatives may not have one).
    if (!isset($definition["id"]) || $definition["id"] !== $plugin_id) {
      $definition["id"] = $plugin_id;
    }

    // Prefix the ID with the provider if available.
    // Note: Derivative plugins contain ":" (e.g., "base:derivative"), but this
    // is different from our provider namespace. We check if the ID already
    // starts with the provider prefix to avoid double-namespacing.
    $provider = $definition["provider"] ?? "";
    if ($provider !== "" && !$this->isProviderNamespaced($plugin_id, $provider)) {
      $definition["id"] = $this->buildNamespacedId($provider, $plugin_id);
    }
  }

  /**
   * {@inheritdoc}
   *
   * Re-keys definitions by their namespaced IDs after processing.
   *
   * @return array<string, array<string, mixed>>
   *   The array of plugin definitions, keyed by namespaced plugin ID.
   */
  protected function findDefinitions(): array {
    $definitions = $this->getDiscovery()->getDefinitions();

    foreach ($definitions as $plugin_id => &$definition) {
      $this->processDefinition($definition, $plugin_id);
    }
    // Unset the reference to avoid PHP foreach reference bug.
    unset($definition);

    // Re-key definitions by the new namespaced ID.
    $reKeyed = [];
    foreach ($definitions as $def) {
      $namespacedId = $def["id"];
      $reKeyed[$namespacedId] = $def;
    }

    $this->alterDefinitions($reKeyed);

    // Remove plugins from non-existent providers.
    foreach ($reKeyed as $plugin_id => $plugin_definition) {
      $provider = $this->extractProviderFromDefinition($plugin_definition);
      if ($provider && !in_array($provider, ["core", "component"]) && !$this->providerExists($provider)) {
        unset($reKeyed[$plugin_id]);
      }
    }

    return $reKeyed;
  }

  /**
   * Builds a namespaced plugin ID from provider and original ID.
   *
   * @param string $provider
   *   The provider module name.
   * @param string $originalId
   *   The original plugin ID.
   *
   * @return string
   *   The namespaced ID in format "provider:original_id".
   */
  public function buildNamespacedId(string $provider, string $originalId): string {
    return $provider . self::NAMESPACE_SEPARATOR . $originalId;
  }

  /**
   * Checks if a plugin ID contains a namespace separator.
   *
   * Note: This only checks for the presence of ":" in the ID. It does NOT
   * distinguish between provider namespacing (e.g., "provider:plugin") and
   * derivative IDs (e.g., "base:derivative"). Use isProviderNamespaced() to
   * check if an ID is already prefixed with a specific provider.
   *
   * @param string $pluginId
   *   The plugin ID to check.
   *
   * @return bool
   *   TRUE if the ID contains the namespace separator.
   */
  public function isNamespaced(string $pluginId): bool {
    return str_contains($pluginId, self::NAMESPACE_SEPARATOR);
  }

  /**
   * Checks if a plugin ID is already namespaced with a specific provider.
   *
   * This method correctly distinguishes between:
   * - Provider-namespaced IDs: "my_module:plugin_id" (already namespaced)
   * - Derivative IDs: "base_plugin:derivative_id" (NOT provider-namespaced)
   *
   * @param string $pluginId
   *   The plugin ID to check.
   * @param string $provider
   *   The provider module name to check for.
   *
   * @return bool
   *   TRUE if the ID starts with the provider prefix.
   */
  public function isProviderNamespaced(string $pluginId, string $provider): bool {
    return str_starts_with($pluginId, $provider . self::NAMESPACE_SEPARATOR);
  }

  /**
   * Extracts the provider from a namespaced plugin ID.
   *
   * @param string $pluginId
   *   The namespaced plugin ID.
   *
   * @return string|null
   *   The provider name, or NULL if the ID is not namespaced.
   */
  public function getProviderFromNamespacedId(string $pluginId): ?string {
    if (!$this->isNamespaced($pluginId)) {
      return NULL;
    }
    $parts = explode(self::NAMESPACE_SEPARATOR, $pluginId, 2);
    return $parts[0];
  }

  /**
   * Extracts the original ID from a namespaced plugin ID.
   *
   * @param string $pluginId
   *   The namespaced plugin ID.
   *
   * @return string
   *   The original plugin ID without the provider prefix.
   */
  public function getOriginalIdFromNamespacedId(string $pluginId): string {
    if (!$this->isNamespaced($pluginId)) {
      return $pluginId;
    }
    $parts = explode(self::NAMESPACE_SEPARATOR, $pluginId, 2);
    return $parts[1] ?? $pluginId;
  }

}
