<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO\NodeMetadata;

use Drupal\flowdrop\DTO\ConfigEdit\ConfigEdit;

/**
 * Value object representing complete node type metadata.
 *
 * Encapsulates all metadata returned by the nodes API, providing
 * type-safe access to node configuration for the workflow editor.
 * This is the primary data structure returned by the NodesController
 * for node type listings and individual node type queries.
 *
 * @see \Drupal\flowdrop_node_type\Controller\Api\NodesController
 *
 * @api
 */
final class NodeMetadata {

  /**
   * Constructs a NodeMetadata instance.
   *
   * @param string $id
   *   The node type ID (e.g., "trigger", "text_processor").
   * @param string $name
   *   The human-readable node name.
   * @param string $type
   *   The visual type (e.g., "basic", "terminal").
   * @param array<string> $supportedTypes
   *   List of supported visual types for this node.
   * @param string $description
   *   The node description.
   * @param string $category
   *   The category in plural form (e.g., "triggers", "processing").
   * @param string $icon
   *   The icon identifier (e.g., "mdi:play", "mdi:cog").
   * @param string $pluginVersion
   *   The plugin version string.
   * @param bool $enabled
   *   Whether the node type is enabled.
   * @param array<string> $tags
   *   Associated tags for categorization/filtering.
   * @param string $executorPlugin
   *   The executor plugin ID that processes this node.
   * @param array<NodePort> $inputs
   *   The input ports for this node.
   * @param array<NodePort> $outputs
   *   The output ports for this node.
   * @param array<string, mixed> $config
   *   Default configuration values.
   * @param array<string, mixed> $configSchema
   *   The JSON Schema for configuration.
   * @param \Drupal\flowdrop\DTO\ConfigEdit\ConfigEdit|null $configEdit
   *   Optional configuration edit options.
   */
  public function __construct(
    public readonly string $id,
    public readonly string $name,
    public readonly string $type,
    public readonly array $supportedTypes,
    public readonly string $description,
    public readonly string $category,
    public readonly string $icon,
    public readonly string $pluginVersion,
    public readonly bool $enabled,
    public readonly array $tags,
    public readonly string $executorPlugin,
    public readonly array $inputs,
    public readonly array $outputs,
    public readonly array $config,
    public readonly array $configSchema,
    public readonly ?ConfigEdit $configEdit = NULL,
  ) {}

  /**
   * Creates an instance from an array.
   *
   * Useful for testing and deserializing stored/cached node metadata.
   *
   * @param array<string, mixed> $data
   *   The array data containing node metadata properties.
   *
   * @return self
   *   A new NodeMetadata instance.
   */
  public static function fromArray(array $data): self {
    // Parse input ports.
    $inputs = [];
    if (isset($data["inputs"]) && is_array($data["inputs"])) {
      foreach ($data["inputs"] as $inputData) {
        if (is_array($inputData)) {
          $inputs[] = NodePort::fromArray($inputData);
        }
      }
    }

    // Parse output ports.
    $outputs = [];
    if (isset($data["outputs"]) && is_array($data["outputs"])) {
      foreach ($data["outputs"] as $outputData) {
        if (is_array($outputData)) {
          $outputs[] = NodePort::fromArray($outputData);
        }
      }
    }

    // Parse configEdit if present.
    $configEdit = NULL;
    if (isset($data["configEdit"]) && is_array($data["configEdit"])) {
      $configEdit = ConfigEdit::fromArray($data["configEdit"]);
    }

    // Parse tags - handle both array and null.
    $tags = [];
    if (isset($data["tags"]) && is_array($data["tags"])) {
      $tags = array_map("strval", $data["tags"]);
    }

    // Parse supportedTypes.
    $supportedTypes = [];
    if (isset($data["supportedTypes"]) && is_array($data["supportedTypes"])) {
      $supportedTypes = array_map("strval", $data["supportedTypes"]);
    }

    return new self(
      id: (string) ($data["id"] ?? ""),
      name: (string) ($data["name"] ?? ""),
      type: (string) ($data["type"] ?? "basic"),
      supportedTypes: $supportedTypes,
      description: (string) ($data["description"] ?? ""),
      category: (string) ($data["category"] ?? ""),
      icon: (string) ($data["icon"] ?? ""),
      pluginVersion: (string) ($data["plugin_version"] ?? $data["pluginVersion"] ?? ""),
      enabled: (bool) ($data["enabled"] ?? TRUE),
      tags: $tags,
      executorPlugin: (string) ($data["executor_plugin"] ?? $data["executorPlugin"] ?? ""),
      inputs: $inputs,
      outputs: $outputs,
      config: is_array($data["config"] ?? NULL) ? $data["config"] : [],
      configSchema: is_array($data["configSchema"] ?? NULL) ? $data["configSchema"] : [],
      configEdit: $configEdit,
    );
  }

  /**
   * Converts the object to an array suitable for JSON serialization.
   *
   * The output format matches the existing API response structure
   * to ensure backward compatibility.
   *
   * @return array<string, mixed>
   *   The array representation for API responses.
   */
  public function toArray(): array {
    $result = [
      "id" => $this->id,
      "name" => $this->name,
      "type" => $this->type,
      "supportedTypes" => $this->supportedTypes,
      "description" => $this->description,
      "category" => $this->category,
      "icon" => $this->icon,
      "plugin_version" => $this->pluginVersion,
      "enabled" => $this->enabled,
      "tags" => $this->tags,
      "executor_plugin" => $this->executorPlugin,
      "inputs" => array_map(
        static fn(NodePort $port): array => $port->toArray(),
        $this->inputs
      ),
      "outputs" => array_map(
        static fn(NodePort $port): array => $port->toArray(),
        $this->outputs
      ),
      "config" => $this->config,
      "configSchema" => $this->configSchema,
    ];

    // Only include configEdit if present.
    if ($this->configEdit !== NULL) {
      $result["configEdit"] = $this->configEdit->toArray();
    }

    return $result;
  }

  /**
   * Checks if this node has configEdit options.
   *
   * @return bool
   *   TRUE if configEdit is configured.
   */
  public function hasConfigEdit(): bool {
    return $this->configEdit !== NULL;
  }

  /**
   * Gets an input port by ID.
   *
   * @param string $id
   *   The port ID to find.
   *
   * @return \Drupal\flowdrop\DTO\NodeMetadata\NodePort|null
   *   The matching port or NULL if not found.
   */
  public function getInputById(string $id): ?NodePort {
    foreach ($this->inputs as $port) {
      if ($port->id === $id) {
        return $port;
      }
    }
    return NULL;
  }

  /**
   * Gets an output port by ID.
   *
   * @param string $id
   *   The port ID to find.
   *
   * @return \Drupal\flowdrop\DTO\NodeMetadata\NodePort|null
   *   The matching port or NULL if not found.
   */
  public function getOutputById(string $id): ?NodePort {
    foreach ($this->outputs as $port) {
      if ($port->id === $id) {
        return $port;
      }
    }
    return NULL;
  }

  /**
   * Gets the count of input ports.
   *
   * @return int
   *   The number of input ports.
   */
  public function getInputCount(): int {
    return count($this->inputs);
  }

  /**
   * Gets the count of output ports.
   *
   * @return int
   *   The number of output ports.
   */
  public function getOutputCount(): int {
    return count($this->outputs);
  }

  /**
   * Checks if this node has any required inputs.
   *
   * @return bool
   *   TRUE if at least one input port is required.
   */
  public function hasRequiredInputs(): bool {
    foreach ($this->inputs as $port) {
      if ($port->required) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Gets all required input ports.
   *
   * @return array<NodePort>
   *   Array of required input ports.
   */
  public function getRequiredInputs(): array {
    return array_filter(
      $this->inputs,
      static fn(NodePort $port): bool => $port->required
    );
  }

}
