<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\DTO;

/**
 * The capabilities a workflow requires, bucketed for an import preflight.
 *
 * Modelled on an "asking permissions" screen: the flow declares the executable
 * capabilities it needs, and the manifest sorts them into ready / will-be-
 * generated / install-this-module so the user can grant what is missing before
 * applying. The flow can be applied only when nothing is missing.
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
final class WorkflowCapabilityManifest {

  /**
   * Constructs a WorkflowCapabilityManifest.
   *
   * @param list<\Drupal\flowdrop_workflow\DTO\NodeCapability> $capabilities
   *   One capability per distinct node type, in declaration order.
   */
  public function __construct(
    private readonly array $capabilities,
  ) {}

  /**
   * All capabilities.
   *
   * @return list<\Drupal\flowdrop_workflow\DTO\NodeCapability>
   *   The capabilities.
   */
  public function all(): array {
    return $this->capabilities;
  }

  /**
   * Capabilities that are ready to run.
   *
   * @return list<\Drupal\flowdrop_workflow\DTO\NodeCapability>
   *   The available capabilities.
   */
  public function available(): array {
    return $this->withStatus(NodeCapability::AVAILABLE);
  }

  /**
   * Capabilities that can be provisioned by generating a node type.
   *
   * @return list<\Drupal\flowdrop_workflow\DTO\NodeCapability>
   *   The generatable capabilities.
   */
  public function generatable(): array {
    return $this->withStatus(NodeCapability::GENERATABLE);
  }

  /**
   * Capabilities whose provider module is not installed.
   *
   * @return list<\Drupal\flowdrop_workflow\DTO\NodeCapability>
   *   The missing capabilities.
   */
  public function missing(): array {
    return $this->withStatus(NodeCapability::MISSING);
  }

  /**
   * The distinct provider modules that must be installed before applying.
   *
   * @return list<string>
   *   Module machine names, de-duplicated.
   */
  public function missingModules(): array {
    $modules = [];
    foreach ($this->missing() as $capability) {
      $module = $capability->getProviderModule();
      if ($module !== '' && !in_array($module, $modules, TRUE)) {
        $modules[] = $module;
      }
    }
    return $modules;
  }

  /**
   * Whether the workflow can be applied (nothing missing).
   *
   * @return bool
   *   TRUE when no capability is missing.
   */
  public function canApply(): bool {
    return $this->missing() === [];
  }

  /**
   * Converts to an array representation for an API preflight response.
   *
   * @return array{can_apply: bool, missing_modules: list<string>, capabilities: list<array<string, mixed>>}
   *   The array form.
   */
  public function toArray(): array {
    return [
      'can_apply' => $this->canApply(),
      'missing_modules' => $this->missingModules(),
      'capabilities' => array_map(
        static fn (NodeCapability $capability): array => $capability->toArray(),
        $this->capabilities,
      ),
    ];
  }

  /**
   * Filters capabilities by status.
   *
   * @param string $status
   *   The status to keep.
   *
   * @return list<\Drupal\flowdrop_workflow\DTO\NodeCapability>
   *   The matching capabilities.
   */
  private function withStatus(string $status): array {
    return array_values(array_filter(
      $this->capabilities,
      static fn (NodeCapability $capability): bool => $capability->getStatus() === $status,
    ));
  }

}
